initial upload
This commit is contained in:
parent
aa03f35ae0
commit
d61f6c8c94
3
z3/Backend/.dockerignore
Normal file
3
z3/Backend/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.env
|
2
z3/Backend/.env
Normal file
2
z3/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
z3/Backend/Dockerfile
Normal file
6
z3/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
z3/Backend/models/Transaction.js
Normal file
24
z3/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
z3/Backend/package-lock.json
generated
Normal file
1711
z3/Backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
z3/Backend/package.json
Normal file
29
z3/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
z3/Backend/routes/transaction.js
Normal file
75
z3/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
z3/Backend/server.js
Normal file
32
z3/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
z3/Frontend/.dockerignore
Normal file
3
z3/Frontend/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.env
|
24
z3/Frontend/.gitignore
vendored
Normal file
24
z3/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?
|
9
z3/Frontend/Dockerfile
Normal file
9
z3/Frontend/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM node:20-bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
EXPOSE 5173
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
50
z3/Frontend/README.md
Normal file
50
z3/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
z3/Frontend/eslint.config.js
Normal file
28
z3/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
z3/Frontend/index.html
Normal file
13
z3/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
z3/Frontend/package-lock.json
generated
Normal file
4364
z3/Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
z3/Frontend/package.json
Normal file
41
z3/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
z3/Frontend/postcss.config.js
Normal file
6
z3/Frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
1
z3/Frontend/public/vite.svg
Normal file
1
z3/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
z3/Frontend/src/App.css
Normal file
0
z3/Frontend/src/App.css
Normal file
28
z3/Frontend/src/App.tsx
Normal file
28
z3/Frontend/src/App.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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() {
|
||||||
|
// Используем window.location.pathname для определения текущего пути
|
||||||
|
// В Azure Container Apps приложение может быть не в корне
|
||||||
|
const getBasename = () => {
|
||||||
|
// Возвращаем пустую строку, если невозможно определить путь
|
||||||
|
return '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BrowserRouter basename={getBasename()}>
|
||||||
|
<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
z3/Frontend/src/Components/Chart/IncomeChart.tsx
Normal file
141
z3/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 = () => {
|
||||||
|
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
z3/Frontend/src/Components/DashboardPage/Dashboard.tsx
Normal file
13
z3/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
z3/Frontend/src/Components/DashboardPage/DashboardPage.tsx
Normal file
19
z3/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
z3/Frontend/src/Components/Expenses/Expenses.tsx
Normal file
43
z3/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
z3/Frontend/src/Components/ExpensesPage/ExpensesPage.tsx
Normal file
15
z3/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
z3/Frontend/src/Components/Header/Header.tsx
Normal file
26
z3/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
z3/Frontend/src/Components/Hero/Hero.tsx
Normal file
9
z3/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
z3/Frontend/src/Components/HomePage/HomePage.tsx
Normal file
15
z3/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
z3/Frontend/src/Components/Income/Income.tsx
Normal file
36
z3/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
z3/Frontend/src/Components/IncomePage/IncomePage.tsx
Normal file
14
z3/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
z3/Frontend/src/Components/Ledger/Ledger.tsx
Normal file
64
z3/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 = () => {
|
||||||
|
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
z3/Frontend/src/Components/addTransaction/addTransaction.tsx
Normal file
103
z3/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 = () => {
|
||||||
|
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
z3/Frontend/src/Routes/Routes.tsx
Normal file
17
z3/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
z3/Frontend/src/api.ts
Normal file
45
z3/Frontend/src/api.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// frontend/src/api.ts
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Получаем URL API из переменной окружения или используем локальный URL по умолчанию
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
|
||||||
|
const API_URL = `${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_URL);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавление новой транзакции
|
||||||
|
export const addTransaction = async (transaction: Omit<Transaction, '_id'>) => {
|
||||||
|
const response = await axios.post<Transaction>(API_URL, transaction);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Удаление транзакции по ID
|
||||||
|
export const deleteTransactionById = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`${API_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_URL}/${id}`, updatedTransaction);
|
||||||
|
return response.data;
|
||||||
|
};
|
1
z3/Frontend/src/assets/react.svg
Normal file
1
z3/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
z3/Frontend/src/index.css
Normal file
3
z3/Frontend/src/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities";
|
13
z3/Frontend/src/main.tsx
Normal file
13
z3/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
z3/Frontend/src/redux/features/balanceSlice.ts
Normal file
119
z3/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
z3/Frontend/src/redux/store.ts
Normal file
16
z3/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;
|
10
z3/Frontend/src/vite-env.d.ts
vendored
Normal file
10
z3/Frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
// добавьте другие переменные окружения по необходимости
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
26
z3/Frontend/tailwind.config.js
Normal file
26
z3/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
z3/Frontend/tsconfig.app.json
Normal file
24
z3/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
z3/Frontend/tsconfig.json
Normal file
7
z3/Frontend/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
22
z3/Frontend/tsconfig.node.json
Normal file
22
z3/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"]
|
||||||
|
}
|
10
z3/Frontend/vite.config.ts
Normal file
10
z3/Frontend/vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
server:{
|
||||||
|
host:true
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
95
z3/README.md
Normal file
95
z3/README.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# Sledovač výdavkov Web App na Azure
|
||||||
|
|
||||||
|
Toto je webová aplikácia na sledovanie výdavkov, nasadená v cloude Azure s využitím kontajnerizácie a spravovaných služieb.
|
||||||
|
|
||||||
|
## Popis aplikácie
|
||||||
|
|
||||||
|
Sledovač výdavkov je plnohodnotná webová aplikácia na správu osobných financií, ktorá umožňuje:
|
||||||
|
- Pridávať, odstraňovať a upravovať finančné transakcie
|
||||||
|
- Prezerať históriu transakcií
|
||||||
|
- Sledovať výdavky podľa kategórií
|
||||||
|
|
||||||
|
## Architektúra riešenia
|
||||||
|
|
||||||
|
Aplikácia pozostáva z nasledujúcich komponentov:
|
||||||
|
|
||||||
|
1. **Frontend**: React aplikácia nasadená v Azure Container Apps
|
||||||
|
- Vyvinutá pomocou React, TypeScript a Vite
|
||||||
|
- Poskytuje používateľské rozhranie na správu finančných transakcií
|
||||||
|
|
||||||
|
2. **Backend API**: Node.js/Express API nasadené v Azure Container Apps
|
||||||
|
- Poskytuje RESTful API na prácu s transakciami
|
||||||
|
- Komunikuje s MongoDB databázou
|
||||||
|
|
||||||
|
3. **Databáza**: Azure Cosmos DB s MongoDB API
|
||||||
|
- Zabezpečuje trvalé ukladanie údajov
|
||||||
|
- Využíva MongoDB API pre kompatibilitu s existujúcim kódom
|
||||||
|
|
||||||
|
## Použité služby Azure
|
||||||
|
|
||||||
|
- **Azure Container Apps**: Na nasadenie Frontend a Backend aplikácií ako kontajnerov
|
||||||
|
- **Azure Container Registry**: Na ukladanie a správu kontajnerových obrazov
|
||||||
|
- **Azure Cosmos DB**: Na ukladanie údajov s využitím MongoDB API
|
||||||
|
- **Azure Managed Certificates**: Na zabezpečenie HTTPS prístupu s platným certifikátom
|
||||||
|
|
||||||
|
## Popis súborov riešenia
|
||||||
|
|
||||||
|
- `prepare-app.sh`: Skript na prípravu a nasadenie aplikácie v Azure
|
||||||
|
- Vytvára skupinu zdrojov
|
||||||
|
- Vytvára a konfiguruje Azure Container Registry
|
||||||
|
- Vytvára databázu Azure Cosmos DB s MongoDB API
|
||||||
|
- Zostavuje a nahráva Docker obrazy
|
||||||
|
- Vytvára a konfiguruje Container Apps pre Frontend a Backend
|
||||||
|
- Nastavuje HTTPS
|
||||||
|
|
||||||
|
- `remove-app.sh`: Skript na odstránenie všetkých vytvorených zdrojov z Azure
|
||||||
|
- Úplne odstraňuje celú skupinu zdrojov
|
||||||
|
- Zabezpečuje čisté odstránenie bez zvyškových zdrojov
|
||||||
|
|
||||||
|
- `Frontend/` a `Backend/`: Adresáre so zdrojovým kódom aplikácie
|
||||||
|
- Obsahujú Dockerfile pre kontajnerizáciu
|
||||||
|
- Sú nakonfigurované na prácu v cloudovom prostredí
|
||||||
|
|
||||||
|
## Pokyny na použitie
|
||||||
|
|
||||||
|
### Predpoklady
|
||||||
|
|
||||||
|
1. Nainštalované a nakonfigurované Azure CLI
|
||||||
|
2. Účet v Microsoft Azure s dostatočnými právami
|
||||||
|
3. Lokálne nainštalovaný Docker
|
||||||
|
|
||||||
|
### Nasadenie aplikácie
|
||||||
|
|
||||||
|
1. Prihláste sa do Azure CLI:
|
||||||
|
```
|
||||||
|
az login
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Spustite skript prípravy:
|
||||||
|
```
|
||||||
|
chmod +x prepare-app.sh
|
||||||
|
./prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Po úspešnom nasadení dostanete URL adresy na prístup k frontendu a backendu.
|
||||||
|
|
||||||
|
### Používanie aplikácie
|
||||||
|
|
||||||
|
1. Prejdite na URL adresu frontendu, uvedenú vo výstupe skriptu prepare-app.sh
|
||||||
|
2. Začnite pridávať, upravovať a sledovať svoje finančné transakcie
|
||||||
|
|
||||||
|
### Odstránenie aplikácie
|
||||||
|
|
||||||
|
Pre úplné odstránenie všetkých vytvorených zdrojov vykonajte:
|
||||||
|
|
||||||
|
```
|
||||||
|
chmod +x remove-app.sh
|
||||||
|
./remove-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technické detaily
|
||||||
|
|
||||||
|
- **Automatický reštart**: Kontajnery sú nakonfigurované na automatický reštart pri zlyhaní
|
||||||
|
- **Škálovanie**: Nakonfigurované automatické škálovanie od 1 do 3 replík
|
||||||
|
- **Trvalé ukladanie**: Údaje sú uložené v Azure Cosmos DB
|
||||||
|
- **Bezpečnosť**: Všetky spojenia sú zabezpečené HTTPS s platným certifikátom
|
40
z3/docker-compose.yml
Normal file
40
z3/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
|
87
z3/prepare-app.sh
Normal file
87
z3/prepare-app.sh
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
RESOURCE_GROUP="expense-tracker-group"
|
||||||
|
LOCATION="northeurope"
|
||||||
|
ACR_NAME="expensetrackercr"
|
||||||
|
CONTAINER_APP_ENV="expense-tracker-env"
|
||||||
|
FRONTEND_APP_NAME="expense-tracker-frontend"
|
||||||
|
BACKEND_APP_NAME="expense-tracker-backend"
|
||||||
|
MONGO_URI="mongodb+srv://pervashovandrii:Y4gXPYacgMOwdoTD@cluster0.rtidc.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
|
||||||
|
|
||||||
|
echo "Registering Azure resource providers..."
|
||||||
|
az provider register --namespace Microsoft.App
|
||||||
|
az provider register --namespace Microsoft.OperationalInsights
|
||||||
|
az provider register --namespace Microsoft.ContainerRegistry
|
||||||
|
|
||||||
|
echo "Creating resource group..."
|
||||||
|
az group create --name $RESOURCE_GROUP --location $LOCATION
|
||||||
|
|
||||||
|
echo "Creating Azure Container Registry..."
|
||||||
|
az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Basic --admin-enabled true
|
||||||
|
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query loginServer --output tsv)
|
||||||
|
ACR_USERNAME=$(az acr credential show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query username --output tsv)
|
||||||
|
ACR_PASSWORD=$(az acr credential show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query passwords[0].value --output tsv)
|
||||||
|
|
||||||
|
if [ ! -d "../Backend" ] || [ ! -d "../Frontend" ]; then
|
||||||
|
echo "Error: Backend or Frontend directories not found in parent directory."
|
||||||
|
echo "Please make sure the directory structure is correct and run the script from sk1 directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating Container App Environment..."
|
||||||
|
az containerapp env create --name $CONTAINER_APP_ENV --resource-group $RESOURCE_GROUP --location $LOCATION
|
||||||
|
|
||||||
|
echo "Building and pushing Docker images to ACR..."
|
||||||
|
az acr login --name $ACR_NAME
|
||||||
|
|
||||||
|
echo "Building and pushing Backend image..."
|
||||||
|
docker build -t ${ACR_LOGIN_SERVER}/backend:latest ../Backend
|
||||||
|
docker push ${ACR_LOGIN_SERVER}/backend:latest
|
||||||
|
|
||||||
|
echo "Building and pushing Frontend image..."
|
||||||
|
docker build -t ${ACR_LOGIN_SERVER}/frontend:latest -f ../Frontend/Dockerfile.temp ../Frontend
|
||||||
|
docker push ${ACR_LOGIN_SERVER}/frontend:latest
|
||||||
|
|
||||||
|
rm ../Frontend/Dockerfile.temp
|
||||||
|
|
||||||
|
mkdir -p ../Backend
|
||||||
|
echo "PORT=5000" > ../Backend/.env
|
||||||
|
echo "MONGO_URI=\"${MONGO_URI}\"" >> ../Backend/.env
|
||||||
|
|
||||||
|
echo "Creating Container App for Backend..."
|
||||||
|
az containerapp create \
|
||||||
|
--name $BACKEND_APP_NAME \
|
||||||
|
--resource-group $RESOURCE_GROUP \
|
||||||
|
--environment $CONTAINER_APP_ENV \
|
||||||
|
--image ${ACR_LOGIN_SERVER}/backend:latest \
|
||||||
|
--registry-server $ACR_LOGIN_SERVER \
|
||||||
|
--registry-username $ACR_USERNAME \
|
||||||
|
--registry-password $ACR_PASSWORD \
|
||||||
|
--env-vars "PORT=5000" "MONGO_URI=${MONGO_URI}" \
|
||||||
|
--target-port 5000 \
|
||||||
|
--ingress external \
|
||||||
|
--min-replicas 1 \
|
||||||
|
--max-replicas 3
|
||||||
|
|
||||||
|
BACKEND_URL=$(az containerapp show --name $BACKEND_APP_NAME --resource-group $RESOURCE_GROUP --query properties.configuration.ingress.fqdn -o tsv)
|
||||||
|
|
||||||
|
echo "Creating Container App for Frontend..."
|
||||||
|
az containerapp create \
|
||||||
|
--name $FRONTEND_APP_NAME \
|
||||||
|
--resource-group $RESOURCE_GROUP \
|
||||||
|
--environment $CONTAINER_APP_ENV \
|
||||||
|
--image ${ACR_LOGIN_SERVER}/frontend:latest \
|
||||||
|
--registry-server $ACR_LOGIN_SERVER \
|
||||||
|
--registry-username $ACR_USERNAME \
|
||||||
|
--registry-password $ACR_PASSWORD \
|
||||||
|
--env-vars "VITE_API_URL=https://${BACKEND_URL}" "NODE_ENV=development" \
|
||||||
|
--target-port 5173 \
|
||||||
|
--ingress external \
|
||||||
|
--min-replicas 1 \
|
||||||
|
--max-replicas 3
|
||||||
|
|
||||||
|
FRONTEND_URL=$(az containerapp show --name $FRONTEND_APP_NAME --resource-group $RESOURCE_GROUP --query properties.configuration.ingress.fqdn -o tsv)
|
||||||
|
|
||||||
|
echo "Application deployed successfully!"
|
||||||
|
echo "Frontend URL: https://${FRONTEND_URL}"
|
||||||
|
echo "Backend URL: https://${BACKEND_URL}"
|
18
z3/remove-app.sh
Normal file
18
z3/remove-app.sh
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Конфигурационные переменные
|
||||||
|
RESOURCE_GROUP="expense-tracker-group"
|
||||||
|
|
||||||
|
# Проверяем существование группы ресурсов перед удалением
|
||||||
|
GROUP_EXISTS=$(az group exists --name $RESOURCE_GROUP)
|
||||||
|
|
||||||
|
if [ "$GROUP_EXISTS" = "true" ]; then
|
||||||
|
# Удаляем всю группу ресурсов и все ресурсы в ней
|
||||||
|
echo "Removing all Azure resources..."
|
||||||
|
az group delete --name $RESOURCE_GROUP --yes --no-wait
|
||||||
|
|
||||||
|
echo "Removal command issued. All resources in resource group $RESOURCE_GROUP will be removed."
|
||||||
|
echo "Note: The actual deletion may take a few minutes to complete in Azure."
|
||||||
|
else
|
||||||
|
echo "Resource group $RESOURCE_GROUP does not exist or you don't have permissions to access it."
|
||||||
|
fi
|
Loading…
Reference in New Issue
Block a user