z2 upload

This commit is contained in:
Andrii Pervashov 2025-04-10 08:22:24 +02:00
parent 39ea02e7bf
commit aa03f35ae0
55 changed files with 7734 additions and 0 deletions

3
z2/Backend/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
*.env

2
z2/Backend/.env Normal file
View File

@ -0,0 +1,2 @@
PORT=5000
MONGO_URI="mongodb+srv://pervashovandrii:Y4gXPYacgMOwdoTD@cluster0.rtidc.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"

6
z2/Backend/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM node:20-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

View File

@ -0,0 +1,24 @@
const mongoose = require('mongoose');
const TransactionSchema = new mongoose.Schema({
description: {
type: String,
required: true,
trim: true
},
amount: {
type: Number,
required: true
},
category: {
type: String,
required: true,
enum: ['Food', 'Entertainment', 'Transport', 'Salary', 'Other'] // Вы можете добавить или изменить категории
},
date: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Transaction', TransactionSchema);

1711
z2/Backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
z2/Backend/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "backend",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"dotenv": "^16.4.5",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"mongoose": "^8.8.0",
"morgan": "~1.9.1"
},
"devDependencies": {
"@types/node": "^22.8.6",
"nodemon": "^3.1.7"
},
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View File

@ -0,0 +1,75 @@
const express = require("express");
const router = express.Router();
const Transaction = require("../models/Transaction");
const mongoose = require('mongoose');
router.get("/", async (req, res) => {
try {
const transactions = await Transaction.find().sort({ date: -1 });
res.json(transactions);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
router.post("/", async (req, res) => {
const { description, amount, category, date } = req.body;
const transaction = new Transaction({
description,
amount,
category,
date,
});
try {
const newTransaction = await transaction.save();
res.status(201).json(newTransaction);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
console.log('ID to be deleted:', id);
const objectId = new mongoose.Types.ObjectId(id);
const transaction = await Transaction.findByIdAndDelete(objectId);
if (!transaction) {
console.log('Transaction not found');
return res.status(404).json({ message: 'Transaction not found' });
}
console.log('Transaction deleted successfully');
res.json({ message: 'Transaction deleted successfully' });
} catch (err) {
console.error('Error deleting the transaction:', err.message);
console.error(err.stack);
res.status(500).json({ message: 'Server error on deletion' });
}
});
router.put("/:id", async (req, res) => {
try {
const { description, amount, category, date } = req.body;
const transaction = await Transaction.findById(req.params.id);
if (!transaction)
return res.status(404).json({ message: "Transaction not found" });
transaction.description = description || transaction.description;
transaction.amount = amount !== undefined ? amount : transaction.amount;
transaction.category = category || transaction.category;
transaction.date = date || transaction.date;
const updatedTransaction = await transaction.save();
res.json(updatedTransaction);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
module.exports = router;

32
z2/Backend/server.js Normal file
View File

@ -0,0 +1,32 @@
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const dotenv = require("dotenv");
const transactionsRouter = require("./routes/transaction");
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("MongoDB connected"))
.catch((err) => console.error(err));
app.use("/api/transactions", transactionsRouter);
app.get("/", (req, res) => {
res.send("Finance tracker API!!");
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server is running on ${PORT}`);
});

View File

@ -0,0 +1,3 @@
node_modules
dist
*.env

24
z2/Frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

10
z2/Frontend/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:20-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Создаем переменную окружения для указания, что мы в Kubernetes
ENV VITE_K8S_ENV=true
EXPOSE 5173
# Используем прокси-сервер Vite
CMD ["npm", "run", "dev", "--", "--host"]

50
z2/Frontend/README.md Normal file
View File

@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
z2/Frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4364
z2/Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
z2/Frontend/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "expencetracker",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.2.7",
"axios": "^1.7.7",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.4",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.47",
"sass": "^1.79.4",
"tailwindcss": "^3.4.14",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

0
z2/Frontend/src/App.css Normal file
View File

20
z2/Frontend/src/App.tsx Normal file
View File

@ -0,0 +1,20 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import HomePage from "./Components/HomePage/HomePage";
import Dashboard from "./Components/DashboardPage/DashboardPage";
import ExpensesPage from "./Components/ExpensesPage/ExpensesPage";
import IncomePage from "./Components/IncomePage/IncomePage";
import './index.css';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage/>}/>
<Route path="dashboard" element={<Dashboard/>}/>
<Route path="expenses" element={<ExpensesPage/>}/>
<Route path="income" element={<IncomePage/>}/>
</Routes>
</BrowserRouter>
);
};
export default App;

View File

@ -0,0 +1,141 @@
import React, { useState } from "react";
import { useSelector } from "react-redux";
import {
Chart,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ArcElement,
} from "chart.js";
import { Bar, Pie } from "react-chartjs-2";
import { RootState } from "../../redux/store";
Chart.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ArcElement
);
const IncomeExpenseChart: React.FC = () => {
const transactions = useSelector(
(state: RootState) => state.balance.transactions
);
const [view, setView] = useState<"monthly" | "categories">("monthly");
const incomeTransactions = transactions.filter(
(transaction) => transaction.amount > 0
);
const expenseTransactions = transactions.filter(
(transaction) => transaction.amount < 0
);
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const monthlyIncomeData = months.map((month, index) => {
return incomeTransactions
.filter((transaction) => new Date(transaction.date).getMonth() === index)
.reduce((acc, transaction) => acc + transaction.amount, 0);
});
const monthlyExpenseData = months.map((month, index) => {
return expenseTransactions
.filter((transaction) => new Date(transaction.date).getMonth() === index)
.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0);
});
const monthlyChartData = {
labels: months,
datasets: [
{
label: "Income",
data: monthlyIncomeData,
backgroundColor: "rgba(75, 192, 192, 0.6)",
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 1,
},
{
label: "Expenses",
data: monthlyExpenseData,
backgroundColor: "rgba(255, 99, 132, 0.6)",
borderColor: "rgba(255, 99, 132, 1)",
borderWidth: 1,
},
],
};
const categories = Array.from(
new Set(transactions.map((transaction) => transaction.category))
);
const categoryIncomeData = categories.map((category) => {
return incomeTransactions
.filter((transaction) => transaction.category === category)
.reduce((acc, transaction) => acc + transaction.amount, 0);
});
const categoryExpenseData = categories.map((category) => {
return expenseTransactions
.filter((transaction) => transaction.category === category)
.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0);
});
const categoryChartData = {
labels: categories,
datasets: [
{
label: "Income",
data: categoryIncomeData,
backgroundColor: categories.map(() => "rgba(75, 192, 192, 0.6)"),
borderColor: categories.map(() => "rgba(75, 192, 192, 1)"),
borderWidth: 1,
},
{
label: "Expenses",
data: categoryExpenseData,
backgroundColor: categories.map(() => "rgba(255, 99, 132, 0.6)"),
borderColor: categories.map(() => "rgba(255, 99, 132, 1)"),
borderWidth: 1,
},
],
};
return (
<div className="p-4 max-w-md mx-auto bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-4">Income and expenses chart</h2>
<div className="mb-4">
<button
onClick={() => setView(view === "monthly" ? "categories" : "monthly")}
className="mr-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
{view === "monthly" ? "By categorie" : "By month"}
</button>
</div>
{view === "monthly" ? (
<Bar data={monthlyChartData} />
) : (
<Pie data={categoryChartData} />
)}
</div>
);
};
export default IncomeExpenseChart;

View File

@ -0,0 +1,13 @@
import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../redux/store";
const Dashboard = () => {
const balance = useSelector((state: RootState) => state.balance.total);
return(
<div>
<h2 className="text-xl font-bold mb-4">Total balance: {balance}</h2>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,19 @@
import React from "react";
import Header from "../Header/Header";
import AddTransaction from "../addTransaction/addTransaction";
import Ledger from "../Ledger/Ledger";
import IncomeChart from "../Chart/IncomeChart";
const DashboardPage = () => {
return (
<div>
<Header />
<div>
<AddTransaction />
</div>
<Ledger/>
<IncomeChart/>
</div>
);
};
export default DashboardPage;

View File

@ -0,0 +1,43 @@
import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../redux/store";
const Expenses: React.FC = () => {
const transactions = useSelector(
(state: RootState) => state.balance.transactions
);
const expenseTransactions = transactions.filter(
(transaction) => transaction.amount < 0
);
const totalExpenses = expenseTransactions.reduce(
(acc, transaction) => acc + transaction.amount,
0
);
return (
<div className="p-4 max-w-md mx-auto bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-4">Expenses</h2>
<ul>
{expenseTransactions.map((transaction) => (
<li
key={transaction.id}
className="flex justify-between items-center p-2 rounded-md mb-2 bg-red-100 border-l-4 border-red-500"
>
<div>
<span className="font-medium">{transaction.description}</span>
<span className="block text-sm text-gray-600">
{new Date(transaction.date).toLocaleDateString()}
</span>
</div>
<span className="font-bold text-red-700">{transaction.amount}</span>
</li>
))}
</ul>
<div className="mt-4 font-bold">Total: {totalExpenses}</div>
</div>
);
};
export default Expenses;

View File

@ -0,0 +1,15 @@
import React from "react";
import Header from "../Header/Header";
import Expenses from "../Expenses/Expenses";
const ExpensesPage = () => {
return (
<div>
<Header />
<div>
<Expenses/>
</div>
</div>
);
};
export default ExpensesPage;

View File

@ -0,0 +1,26 @@
import React from "react";
import { Link } from "react-router-dom";
const Header = () => {
return (
<header className="bg-blue-600 px-8 py-4 flex items-center justify-between shadow-lg">
<div>
<Link to="/" className="text-white font-bold text-lg">Home</Link>
</div>
<div>
<Link to="/income" className="text-white hover:text-gray-300 font-semibold">Income</Link>
</div>
<div>
<Link to="/expenses" className="text-white hover:text-gray-300 font-semibold">Expenses</Link>
</div>
<div>
<Link to="/dashboard" className="text-white hover:text-gray-300 font-semibold">Dashboard</Link>
</div>
<div>
<Link to="/login" className="text-white hover:text-gray-300 font-semibold">Authorize</Link>
</div>
</header>
);
};
export default Header;

View File

@ -0,0 +1,9 @@
const Hero = () => {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<h1 className="text-6xl font-bold text-gray-800">ExpenseTracker</h1>
</div>
);
};
export default Hero;

View File

@ -0,0 +1,15 @@
import Header from "../Header/Header";
import Hero from "../Hero/Hero";
const HomePage = () => {
return (
<div className="bg-[#629878]">
<Header />
<div>
<Hero />
</div>
</div>
);
};
export default HomePage;

View File

@ -0,0 +1,36 @@
import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../redux/store"; // Импортируем RootState
const Income: React.FC = () => {
const transactions = useSelector((state: RootState) => state.balance.transactions);
// Фильтруем доходы
const incomeTransactions = transactions.filter(transaction => transaction.amount > 0);
// Считаем общую сумму доходов
const totalIncome = incomeTransactions.reduce((acc, transaction) => acc + transaction.amount, 0);
return (
<div className="p-4 max-w-md mx-auto bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-4">Income</h2>
<ul>
{incomeTransactions.map(transaction => (
<li
key={transaction.id}
className="flex justify-between items-center p-2 rounded-md mb-2 bg-green-100 border-l-4 border-green-500"
>
<div>
<span className="font-medium">{transaction.description}</span>
<span className="block text-sm text-gray-600">{new Date(transaction.date).toLocaleDateString()}</span>
</div>
<span className="font-bold text-green-700">+{transaction.amount}</span>
</li>
))}
</ul>
<div className="mt-4 font-bold">Общая сумма: +{totalIncome}</div>
</div>
);
};
export default Income;

View File

@ -0,0 +1,14 @@
import Header from "../Header/Header";
import Income from "../Income/Income";
const IncomePage = () => {
return (
<div>
<Header />
<div>
<Income/>
</div>
</div>
);
};
export default IncomePage;

View File

@ -0,0 +1,64 @@
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState, AppDispatch } from "../../redux/store";
import { fetchAllTransactions, deleteTransaction } from "../../redux/features/balanceSlice";
const Ledger: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
const transactions = useSelector((state: RootState) => state.balance.transactions);
const status = useSelector((state: RootState) => state.balance.status);
const error = useSelector((state: RootState) => state.balance.error);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchAllTransactions());
}
}, [status, dispatch]);
const handleDelete = (id: string) => {
dispatch(deleteTransaction(id));
};
if (status === 'loading') {
return <div>Loading...</div>;
}
if (status === 'failed') {
return <div>Error: {error}</div>;
}
return (
<div className="p-4 max-w-md mx-auto bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-4">Transactions ledger</h2>
<ul>
{transactions.map((transaction) => (
<li
key={transaction._id}
className={`flex justify-between items-center p-2 rounded-md mb-2 ${
transaction.amount > 0 ? "bg-green-100 border-l-4 border-green-500" : "bg-red-100 border-l-4 border-red-500"
}`}
>
<div>
<span className="font-medium">{transaction.description}</span>
<span className="block text-sm text-gray-600">{new Date(transaction.date).toLocaleDateString()}</span>
<span className="block text-sm text-gray-600">{transaction.category}</span>
</div>
<div className="flex items-center">
<span className={`font-bold ${transaction.amount > 0 ? "text-green-700" : "text-red-700"}`}>
{transaction.amount > 0 ? `+${transaction.amount}` : transaction.amount}
</span>
<button
onClick={() => handleDelete(transaction._id)}
className="ml-4 text-red-500 hover:text-red-700"
>
Delete
</button>
</div>
</li>
))}
</ul>
</div>
);
};
export default Ledger;

View File

@ -0,0 +1,103 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addNewTransaction } from "../../redux/features/balanceSlice";
import { AppDispatch } from "../../redux/store";
const AddTransaction: React.FC = () => {
const [description, setDescription] = useState("");
const [amount, setAmount] = useState<number>(0);
const [transactionType, setTransactionType] = useState<"income" | "expense">("income");
const [category, setCategory] = useState<string>("");
const dispatch = useDispatch<AppDispatch>();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newTransaction = {
description,
amount: transactionType === "income" ? amount : -amount,
category,
date: new Date().toISOString(),
};
dispatch(addNewTransaction(newTransaction));
setDescription("");
setAmount(0);
setTransactionType("income");
setCategory("");
};
return (
<form onSubmit={handleSubmit} className="p-4 max-w-sm mx-auto bg-white rounded-lg shadow-md">
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Transaction Type</label>
<div className="flex items-center mb-2">
<input
type="radio"
value="income"
checked={transactionType === "income"}
onChange={() => setTransactionType("income")}
className="mr-2"
/>
<label className="mr-4">Income</label>
<input
type="radio"
value="expense"
checked={transactionType === "expense"}
onChange={() => setTransactionType("expense")}
className="mr-2"
/>
<label>Expense</label>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Description</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="mt-1 p-2 block w-full border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500"
placeholder="Write a description"
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Category</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="mt-1 p-2 block w-full border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500"
required
>
<option value="">Choose category</option>
<option value="Food">Food</option>
<option value="Entertainment">Entertainment</option>
<option value="Транспорт">Transport</option>
<option value="Transport">Salary</option>
<option value="Other">Other</option>
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Sum</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
className="mt-1 p-2 block w-full border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500"
placeholder="Input the value"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
>
Add transaction
</button>
</form>
);
};
export default AddTransaction;

View File

@ -0,0 +1,17 @@
import { createBrowserRouter } from "react-router-dom";
import App from "../App";
import HomePage from "../Components/HomePage/HomePage";
import LoginPage from "../Components/LoginPage/LoginPage";
import RegisterPage from "../Components/RegisterPage/RegisterPage";
import Dashboard from "../Components/DashboardPage/DashboardPage";
export const router = createBrowserRouter([{
path:"/",
element: <App/>,
children:[
{path:"", element: <HomePage/>},
{path:"login", element: <LoginPage/>},
{path: "register", element: <RegisterPage/>},
{path: "dashboard", element: <Dashboard/>}
]
}])

45
z2/Frontend/src/api.ts Normal file
View File

@ -0,0 +1,45 @@
// frontend/src/api.ts
import axios from 'axios';
// В Kubernetes у нас Frontend доступен через NodePort,
// поэтому запросы к бэкенду должны идти на относительный путь
const API_BASE_URL = '/api/transactions';
// Определение интерфейса Transaction
export interface Transaction {
_id: string;
description: string;
amount: number;
category: string;
date: string;
}
// Получение всех транзакций
export const getTransactions = async () => {
const response = await axios.get<Transaction[]>(API_BASE_URL);
return response.data;
};
// Добавление новой транзакции
export const addTransaction = async (transaction: Omit<Transaction, '_id'>) => {
const response = await axios.post<Transaction>(API_BASE_URL, transaction);
return response.data;
};
// Удаление транзакции по ID
export const deleteTransactionById = async (id: string) => {
try {
const response = await axios.delete(`${API_BASE_URL}/${id}`);
return response.data;
} catch (error) {
console.error('Ошибка при удалении транзакции:', error);
throw error;
}
};
// Обновление транзакции по ID
export const updateTransactionById = async (id: string, updatedTransaction: Partial<Transaction>) => {
const response = await axios.put<Transaction>(`${API_BASE_URL}/${id}`, updatedTransaction);
return response.data;
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

13
z2/Frontend/src/main.tsx Normal file
View File

@ -0,0 +1,13 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "../src/redux/store";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);

View File

@ -0,0 +1,119 @@
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
import {
getTransactions,
addTransaction,
deleteTransactionById,
updateTransactionById,
} from "../../api";
interface Transaction {
_id: string;
description: string;
amount: number;
category: string;
date: string;
}
interface BalanceState {
transactions: Transaction[];
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
}
const initialState: BalanceState = {
transactions: [],
status: "idle",
error: null,
};
export const fetchAllTransactions = createAsyncThunk(
"balance/fetchAllTransactions",
async () => {
const response = await getTransactions();
return response as Transaction[];
}
);
export const addNewTransaction = createAsyncThunk(
"balance/addNewTransaction",
async (transaction: Omit<Transaction, "_id">) => {
const response = await addTransaction(transaction);
return response as Transaction;
}
);
export const deleteTransaction = createAsyncThunk(
"balance/deleteTransaction",
async (id: string) => {
await deleteTransactionById(id);
return id;
}
);
export const updateTransaction = createAsyncThunk(
"balance/updateTransaction",
async ({
id,
updatedTransaction,
}: {
id: string;
updatedTransaction: Partial<Transaction>;
}) => {
const response = await updateTransactionById(id, updatedTransaction);
return response as Transaction;
}
);
const balanceSlice = createSlice({
name: "balance",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// Получение транзакций
.addCase(fetchAllTransactions.pending, (state) => {
state.status = "loading";
})
.addCase(
fetchAllTransactions.fulfilled,
(state, action: PayloadAction<Transaction[]>) => {
state.status = "succeeded";
state.transactions = action.payload;
}
)
.addCase(fetchAllTransactions.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message || "Что-то пошло не так";
})
// Добавление транзакции
.addCase(
addNewTransaction.fulfilled,
(state, action: PayloadAction<Transaction>) => {
state.transactions.push(action.payload);
}
)
// Удаление транзакции
.addCase(
deleteTransaction.fulfilled,
(state, action: PayloadAction<string>) => {
state.transactions = state.transactions.filter(
(transaction) => transaction._id !== action.payload
);
}
)
// Обновление транзакции
.addCase(
updateTransaction.fulfilled,
(state, action: PayloadAction<Transaction>) => {
const index = state.transactions.findIndex(
(transaction) => transaction._id === action.payload._id
);
if (index !== -1) {
state.transactions[index] = action.payload;
}
}
);
},
});
export default balanceSlice.reducer;

View File

@ -0,0 +1,16 @@
// frontend/src/store.ts
import { configureStore } from "@reduxjs/toolkit";
import balanceReducer from "../redux/features/balanceSlice";
const store = configureStore({
reducer: {
balance: balanceReducer,
},
});
// Типизация RootState и AppDispatch
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;

1
z2/Frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,26 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
screens: {
sm: "480px",
md: "768px",
lg: "1020px",
xl: "1440px",
},
extend: {
colors: {
lightBlue: "hsl(215.02, 98.39%, 51.18%)",
darkBlue: "hsl(213.86, 58.82%, 46.67%)",
lightGreen: "hsl(156.62, 73.33%, 58.82%)",
},
fontFamily: {
sans: ["Poppins", "sans-serif"],
},
spacing: {
180: "32rem",
},
},
},
plugins: [],
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
server:{
host: true,
proxy: {
'/api': {
target: 'http://backend-service:5000',
changeOrigin: true,
}
}
},
plugins: [react()],
})

173
z2/README.md Normal file
View File

@ -0,0 +1,173 @@
# Expense Tracker - Kubernetes Deployment
## Описание приложения
Expense Tracker - это веб-приложение для отслеживания личных финансов и управления расходами. Приложение позволяет пользователям:
- Добавлять новые транзакции с указанием суммы, типа (доход/расход) и категории
- Просматривать историю транзакций
- Анализировать расходы по категориям
- Отслеживать баланс счета
## Архитектура приложения
Приложение построено на стеке MERN (MongoDB, Express.js, React, Node.js) и состоит из трех основных компонентов:
1. **Frontend** - клиентская часть, разработанная на React с использованием Vite для быстрой сборки
2. **Backend** - серверная часть, разработанная на Node.js с использованием Express.js
3. **MongoDB** - база данных для хранения информации о транзакциях
## Список использованных контейнеров
1. **expense-tracker-frontend** - контейнер с клиентской частью приложения
- Базовый образ: node:20-bookworm-slim
- Роль: Обслуживает пользовательский интерфейс приложения
- Порт: 5173
2. **expense-tracker-backend** - контейнер с серверной частью приложения
- Базовый образ: node:20-bookworm-slim
- Роль: Предоставляет API для работы с данными о транзакциях
- Порт: 5000
3. **mongo** - контейнер с базой данных MongoDB
- Базовый образ: mongo:latest
- Роль: Хранение данных приложения
- Порт: 27017
## Kubernetes объекты и их описание
1. **Namespace**
- Имя: expense-tracker
- Роль: Изолированное пространство имен для всех ресурсов приложения
2. **Deployment**
- Frontend Deployment: Управляет репликами Pod с клиентской частью приложения
- Backend Deployment: Управляет репликами Pod с серверной частью приложения
3. **StatefulSet**
- MongoDB StatefulSet: Управляет Pod с базой данных MongoDB, сохраняя состояние и данные
4. **Service**
- Frontend Service (NodePort): Предоставляет доступ к клиентской части приложения извне кластера
- Backend Service (ClusterIP): Обеспечивает внутрикластерный доступ к API
- MongoDB Service (ClusterIP): Обеспечивает внутрикластерный доступ к базе данных
5. **PersistentVolume и PersistentVolumeClaim**
- MongoDB PV/PVC: Обеспечивает постоянное хранилище для данных MongoDB
## Сетевая инфраструктура
Приложение использует следующие сетевые ресурсы:
1. **Services**
- Frontend Service (NodePort): Доступен извне кластера через порт, назначаемый Kubernetes (30000-32767)
- Backend Service (ClusterIP): Доступен внутри кластера по имени `backend-service:5000`
- MongoDB Service (ClusterIP): Доступен внутри кластера по имени `mongodb-service:27017`
2. **Внутрикластерная DNS-система**
- Позволяет компонентам приложения обращаться друг к другу по именам сервисов вместо IP-адресов
## Хранилища данных
Приложение использует постоянное хранилище для базы данных MongoDB:
1. **PersistentVolume**
- Тип: HostPath (для локальной разработки)
- Размер: 1Gi
- Путь на хосте: `/mnt/data/mongodb`
2. **PersistentVolumeClaim**
- Запрашивает 1Gi постоянного хранилища для MongoDB
## Конфигурация контейнеров
1. **Frontend**
- Environment: Для разработки используется Vite Dev Server с опцией `--host` для доступа извне
- Ресурсы: Лимиты CPU: 0.5, Memory: 512Mi; Запросы CPU: 0.2, Memory: 256Mi
2. **Backend**
- Environment:
- PORT: 5000
- MONGO_URI: mongodb://mongodb-service:27017/expense-tracker
- Ресурсы: Лимиты CPU: 0.5, Memory: 512Mi; Запросы CPU: 0.2, Memory: 256Mi
3. **MongoDB**
- Без дополнительной конфигурации, используется стандартный образ
- Данные хранятся в постоянном хранилище
- Ресурсы: Лимиты CPU: 0.5, Memory: 512Mi; Запросы CPU: 0.2, Memory: 256Mi
## Инструкция по запуску
### Предварительные требования
1. Установленный и настроенный Kubernetes кластер (можно использовать Minikube для локальной разработки)
2. Установленный и настроенный kubectl
3. Docker
### Подготовка приложения
1. Выполните скрипт подготовки:
```bash
chmod +x prepare-app.sh
./prepare-app.sh
```
Скрипт выполнит следующие действия:
- Создаст директорию для хранения данных MongoDB
- Соберет Docker-образы для Frontend и Backend
### Запуск приложения
1. Выполните скрипт запуска:
```bash
chmod +x start-app.sh
./start-app.sh
```
Скрипт выполнит следующие действия:
- Создаст Namespace expense-tracker
- Применит все конфигурационные файлы Kubernetes
- Дождется запуска всех компонентов
- Выведет URL для доступа к приложению
### Остановка приложения
1. Для остановки приложения выполните:
```bash
chmod +x stop-app.sh
./stop-app.sh
```
Скрипт выполнит следующие действия:
- Удалит все развернутые в Kubernetes ресурсы приложения
### Доступ к приложению
После успешного запуска вы получите URL для доступа к приложению в выводе скрипта start-app.sh. Обычно это будет что-то вроде:
```
http://192.168.49.2:30080
```
где:
- 192.168.49.2 - IP-адрес узла кластера (для Minikube это IP-адрес виртуальной машины)
- 30080 - назначенный NodePort для Frontend-сервиса
## Дополнительная информация
- Для просмотра всех ресурсов в пространстве имен expense-tracker:
```bash
kubectl get all -n expense-tracker
```
- Для просмотра логов компонентов:
```bash
kubectl logs deployment/frontend -n expense-tracker
kubectl logs deployment/backend -n expense-tracker
kubectl logs statefulset/mongodb -n expense-tracker
```
- Для доступа к оболочке контейнеров:
```bash
kubectl exec -it deployment/frontend -n expense-tracker -- sh
kubectl exec -it deployment/backend -n expense-tracker -- sh
kubectl exec -it statefulset/mongodb -n expense-tracker -- sh
```

66
z2/deployment.yaml Normal file
View File

@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: expense-tracker
labels:
app: frontend
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: expense-tracker-frontend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5173
resources:
limits:
cpu: "0.5"
memory: "512Mi"
requests:
cpu: "0.2"
memory: "256Mi"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: expense-tracker
labels:
app: backend
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: expense-tracker-backend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
env:
- name: PORT
value: "5000"
- name: MONGO_URI
value: "mongodb://mongodb-service:27017/expense-tracker"
resources:
limits:
cpu: "0.5"
memory: "512Mi"
requests:
cpu: "0.2"
memory: "256Mi"

40
z2/docker-compose.yml Normal file
View File

@ -0,0 +1,40 @@
name: app
services:
mongo:
image: mongo
ports:
- 27017:27017
volumes:
- mongo_data:/data/db
networks:
- mern-app
api:
build:
context: ./Backend
dockerfile: Dockerfile
restart: unless-stopped
env_file: ./Backend/.env
volumes:
- ./Backend:/app
- /app/node_modules
ports:
- 5000:5000
depends_on:
- mongo
frontend:
build:
context: ./Frontend
dockerfile: Dockerfile
restart: unless-stopped
networks:
- mern-app
ports:
- 5173:5173
command: npm run dev -- --host
depends_on:
- api
volumes:
mongo_data:
networks:
mern-app:
driver: bridge

6
z2/namespace.yaml Normal file
View File

@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: expense-tracker
labels:
app: expense-tracker

16
z2/prepare-app.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
echo "Preparing..."
sudo mkdir -p /mnt/data/mongodb
sudo chmod 777 /mnt/data/mongodb
echo "Building the Frontend..."
docker build --no-cache -t expense-tracker-frontend:latest ./Frontend
echo "Building the Backend..."
docker build --no-cache -t expense-tracker-backend:latest ./Backend
echo "Completed!"

11
z2/remove-app.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
set -e
echo "Deleting..."
bash ./stop-app.sh
sudo rm -rf /mnt/data/mongodb 2>/dev/null || true
echo "Deleted!"

64
z2/service.yaml Normal file
View File

@ -0,0 +1,64 @@
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: expense-tracker
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 5173
type: NodePort
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: expense-tracker-ingress
namespace: expense-tracker
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /()
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- path: /api(/|$)(.*)
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 5000
---
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: expense-tracker
spec:
selector:
app: backend
ports:
- port: 5000
targetPort: 5000
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: mongodb-service
namespace: expense-tracker
spec:
selector:
app: mongodb
ports:
- port: 27017
targetPort: 27017
type: ClusterIP

29
z2/start-app.sh Normal file
View File

@ -0,0 +1,29 @@
#!/bin/bash
set -e
echo "Starting the app..."
kubectl apply -f namespace.yaml
kubectl apply -f statefulset.yaml
kubectl apply -f service.yaml
kubectl apply -f deployment.yaml
echo "Waiting for MongoDB..."
kubectl wait --for=condition=Ready pod -l app=mongodb -n expense-tracker --timeout=120s
echo "Waiting for Backend..."
kubectl wait --for=condition=Ready pod -l app=backend -n expense-tracker --timeout=120s
echo "Waiting for Frontend..."
kubectl wait --for=condition=Ready pod -l app=frontend -n expense-tracker --timeout=120s
echo "Waiting for connectivity...."
NODE_PORT=$(kubectl get service frontend-service -n expense-tracker -o jsonpath='{.spec.ports[0].nodePort}')
MINIKUBE_IP=$(minikube ip 2>/dev/null || echo "localhost")
echo "App started"
echo "Access it at: http://$MINIKUBE_IP:$NODE_PORT"

64
z2/statefulset.yaml Normal file
View File

@ -0,0 +1,64 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: mongodb-pv
namespace: expense-tracker
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data/mongodb"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
namespace: expense-tracker
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb
namespace: expense-tracker
spec:
serviceName: "mongodb-service"
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: mongo:latest
ports:
- containerPort: 27017
volumeMounts:
- name: mongodb-data
mountPath: /data/db
resources:
limits:
cpu: "0.5"
memory: "512Mi"
requests:
cpu: "0.2"
memory: "256Mi"
volumes:
- name: mongodb-data
persistentVolumeClaim:
claimName: mongodb-pvc

15
z2/stop-app.sh Normal file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -e
echo "Stopping the Kubernetes..."
kubectl delete -f deployment.yaml --ignore-not-found=true
kubectl delete -f service.yaml --ignore-not-found=true
kubectl delete -f statefulset.yaml --ignore-not-found=true
kubectl delete -f namespace.yaml --ignore-not-found=true
echo "Deleted!"