z2 upload
This commit is contained in:
parent
39ea02e7bf
commit
aa03f35ae0
3
z2/Backend/.dockerignore
Normal file
3
z2/Backend/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
*.env
|
2
z2/Backend/.env
Normal file
2
z2/Backend/.env
Normal 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
6
z2/Backend/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
CMD ["node", "server.js"]
|
24
z2/Backend/models/Transaction.js
Normal file
24
z2/Backend/models/Transaction.js
Normal 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
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
29
z2/Backend/package.json
Normal 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": ""
|
||||
}
|
75
z2/Backend/routes/transaction.js
Normal file
75
z2/Backend/routes/transaction.js
Normal 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
32
z2/Backend/server.js
Normal 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}`);
|
||||
});
|
3
z2/Frontend/.dockerignore
Normal file
3
z2/Frontend/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
*.env
|
24
z2/Frontend/.gitignore
vendored
Normal file
24
z2/Frontend/.gitignore
vendored
Normal 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
10
z2/Frontend/Dockerfile
Normal 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
50
z2/Frontend/README.md
Normal 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,
|
||||
},
|
||||
})
|
||||
```
|
28
z2/Frontend/eslint.config.js
Normal file
28
z2/Frontend/eslint.config.js
Normal 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
13
z2/Frontend/index.html
Normal 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
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
41
z2/Frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
6
z2/Frontend/postcss.config.js
Normal file
6
z2/Frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
1
z2/Frontend/public/vite.svg
Normal file
1
z2/Frontend/public/vite.svg
Normal 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
0
z2/Frontend/src/App.css
Normal file
20
z2/Frontend/src/App.tsx
Normal file
20
z2/Frontend/src/App.tsx
Normal 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;
|
141
z2/Frontend/src/Components/Chart/IncomeChart.tsx
Normal file
141
z2/Frontend/src/Components/Chart/IncomeChart.tsx
Normal 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;
|
13
z2/Frontend/src/Components/DashboardPage/Dashboard.tsx
Normal file
13
z2/Frontend/src/Components/DashboardPage/Dashboard.tsx
Normal 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;
|
19
z2/Frontend/src/Components/DashboardPage/DashboardPage.tsx
Normal file
19
z2/Frontend/src/Components/DashboardPage/DashboardPage.tsx
Normal 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;
|
43
z2/Frontend/src/Components/Expenses/Expenses.tsx
Normal file
43
z2/Frontend/src/Components/Expenses/Expenses.tsx
Normal 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;
|
15
z2/Frontend/src/Components/ExpensesPage/ExpensesPage.tsx
Normal file
15
z2/Frontend/src/Components/ExpensesPage/ExpensesPage.tsx
Normal 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;
|
26
z2/Frontend/src/Components/Header/Header.tsx
Normal file
26
z2/Frontend/src/Components/Header/Header.tsx
Normal 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;
|
9
z2/Frontend/src/Components/Hero/Hero.tsx
Normal file
9
z2/Frontend/src/Components/Hero/Hero.tsx
Normal 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;
|
15
z2/Frontend/src/Components/HomePage/HomePage.tsx
Normal file
15
z2/Frontend/src/Components/HomePage/HomePage.tsx
Normal 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;
|
36
z2/Frontend/src/Components/Income/Income.tsx
Normal file
36
z2/Frontend/src/Components/Income/Income.tsx
Normal 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;
|
14
z2/Frontend/src/Components/IncomePage/IncomePage.tsx
Normal file
14
z2/Frontend/src/Components/IncomePage/IncomePage.tsx
Normal 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;
|
64
z2/Frontend/src/Components/Ledger/Ledger.tsx
Normal file
64
z2/Frontend/src/Components/Ledger/Ledger.tsx
Normal 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;
|
103
z2/Frontend/src/Components/addTransaction/addTransaction.tsx
Normal file
103
z2/Frontend/src/Components/addTransaction/addTransaction.tsx
Normal 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;
|
17
z2/Frontend/src/Routes/Routes.tsx
Normal file
17
z2/Frontend/src/Routes/Routes.tsx
Normal 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
45
z2/Frontend/src/api.ts
Normal 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;
|
||||
};
|
1
z2/Frontend/src/assets/react.svg
Normal file
1
z2/Frontend/src/assets/react.svg
Normal 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 |
3
z2/Frontend/src/index.css
Normal file
3
z2/Frontend/src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
13
z2/Frontend/src/main.tsx
Normal file
13
z2/Frontend/src/main.tsx
Normal 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>
|
||||
);
|
119
z2/Frontend/src/redux/features/balanceSlice.ts
Normal file
119
z2/Frontend/src/redux/features/balanceSlice.ts
Normal 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;
|
16
z2/Frontend/src/redux/store.ts
Normal file
16
z2/Frontend/src/redux/store.ts
Normal 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
1
z2/Frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
26
z2/Frontend/tailwind.config.js
Normal file
26
z2/Frontend/tailwind.config.js
Normal 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: [],
|
||||
}
|
24
z2/Frontend/tsconfig.app.json
Normal file
24
z2/Frontend/tsconfig.app.json
Normal 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"]
|
||||
}
|
7
z2/Frontend/tsconfig.json
Normal file
7
z2/Frontend/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
22
z2/Frontend/tsconfig.node.json
Normal file
22
z2/Frontend/tsconfig.node.json
Normal 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"]
|
||||
}
|
16
z2/Frontend/vite.config.ts
Normal file
16
z2/Frontend/vite.config.ts
Normal 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
173
z2/README.md
Normal 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
66
z2/deployment.yaml
Normal 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
40
z2/docker-compose.yml
Normal 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
6
z2/namespace.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: expense-tracker
|
||||
labels:
|
||||
app: expense-tracker
|
16
z2/prepare-app.sh
Normal file
16
z2/prepare-app.sh
Normal 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
11
z2/remove-app.sh
Normal 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
64
z2/service.yaml
Normal 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
29
z2/start-app.sh
Normal 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
64
z2/statefulset.yaml
Normal 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
15
z2/stop-app.sh
Normal 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!"
|
Loading…
Reference in New Issue
Block a user