From 38dd167159a2c6d4475d6892fdada925bd0177ec Mon Sep 17 00:00:00 2001 From: tf545su Date: Tue, 19 May 2026 14:34:27 +0200 Subject: [PATCH] adding sk1 --- sk1/.env.example | 1 + sk1/.gitignore | 2 + sk1/backend/Dockerfile | 13 ++ sk1/backend/db.js | 11 ++ sk1/backend/package.json | 14 ++ sk1/backend/server.js | 65 +++++++ sk1/backup/backup.sh | 9 + sk1/backups/backup_2026-05-19_12-37-24.sql | 98 +++++++++++ sk1/db/init.sql | 7 + sk1/docker-compose.yaml | 50 ++++++ sk1/frontend/Dockerfile | 13 ++ sk1/frontend/package.json | 14 ++ sk1/frontend/public/index.html | 10 ++ sk1/frontend/src/App.css | 111 ++++++++++++ sk1/frontend/src/App.js | 186 +++++++++++++++++++++ sk1/frontend/src/index.js | 13 ++ sk1/nginx/default.conf | 12 ++ sk1/prepare-app.sh | 3 + sk1/remove-app.sh | 3 + 19 files changed, 635 insertions(+) create mode 100644 sk1/.env.example create mode 100644 sk1/.gitignore create mode 100644 sk1/backend/Dockerfile create mode 100644 sk1/backend/db.js create mode 100644 sk1/backend/package.json create mode 100644 sk1/backend/server.js create mode 100755 sk1/backup/backup.sh create mode 100644 sk1/backups/backup_2026-05-19_12-37-24.sql create mode 100644 sk1/db/init.sql create mode 100644 sk1/docker-compose.yaml create mode 100644 sk1/frontend/Dockerfile create mode 100644 sk1/frontend/package.json create mode 100644 sk1/frontend/public/index.html create mode 100644 sk1/frontend/src/App.css create mode 100644 sk1/frontend/src/App.js create mode 100644 sk1/frontend/src/index.js create mode 100644 sk1/nginx/default.conf create mode 100755 sk1/prepare-app.sh create mode 100755 sk1/remove-app.sh diff --git a/sk1/.env.example b/sk1/.env.example new file mode 100644 index 0000000..4295075 --- /dev/null +++ b/sk1/.env.example @@ -0,0 +1 @@ +POSTGRES_PASSWORD=changeme diff --git a/sk1/.gitignore b/sk1/.gitignore new file mode 100644 index 0000000..6ed48a9 --- /dev/null +++ b/sk1/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules diff --git a/sk1/backend/Dockerfile b/sk1/backend/Dockerfile new file mode 100644 index 0000000..5c99f6f --- /dev/null +++ b/sk1/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20 + +WORKDIR /app + +COPY package.json . + +RUN npm install + +COPY . . + +EXPOSE 5000 + +CMD ["npm", "start"] diff --git a/sk1/backend/db.js b/sk1/backend/db.js new file mode 100644 index 0000000..51fe44b --- /dev/null +++ b/sk1/backend/db.js @@ -0,0 +1,11 @@ +const { Pool } = require("pg"); + +const pool = new Pool({ + user: "budget_user", + host: "postgres", + database: "budget_db", + password: process.env.POSTGRES_PASSWORD, + port: 5432, +}); + +module.exports = pool; diff --git a/sk1/backend/package.json b/sk1/backend/package.json new file mode 100644 index 0000000..a8b7a4b --- /dev/null +++ b/sk1/backend/package.json @@ -0,0 +1,14 @@ +{ + "name": "budget-backend", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "pg": "^8.11.5" + } +} diff --git a/sk1/backend/server.js b/sk1/backend/server.js new file mode 100644 index 0000000..f6d2572 --- /dev/null +++ b/sk1/backend/server.js @@ -0,0 +1,65 @@ +const express = require("express"); +const cors = require("cors"); +const dotenv = require("dotenv"); +const pool = require("./db"); + +dotenv.config(); + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +app.get("/transactions", async (req, res) => { + try { + const result = await pool.query( + "SELECT * FROM transactions ORDER BY created_at DESC" + ); + + res.json(result.rows); + + } catch (err) { + console.error(err.message); + } +}); + +app.post("/transactions", async (req, res) => { + + try { + + const { title, amount, type } = req.body; + + const result = await pool.query( + "INSERT INTO transactions (title, amount, type) VALUES ($1, $2, $3) RETURNING *", + [title, amount, type] + ); + + res.json(result.rows[0]); + + } catch (err) { + console.error(err.message); + } +}); + +app.delete("/transactions/:id", async (req, res) => { + + try { + + const { id } = req.params; + + await pool.query( + "DELETE FROM transactions WHERE id = $1", + [id] + ); + + res.json("transaction deleted"); + + } catch (err) { + console.error(err.message); + } +}); + +app.listen(5000, () => { + console.log("server running on port 5000"); +}); + diff --git a/sk1/backup/backup.sh b/sk1/backup/backup.sh new file mode 100755 index 0000000..1392d31 --- /dev/null +++ b/sk1/backup/backup.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +DATE=$(date +%Y-%m-%d_%H-%M-%S) + +mkdir -p backups + +docker exec budget_postgres pg_dump -U budget_user budget_db > backups/backup_$DATE.sql + +echo "backup created: backups/backup_$DATE.sql" diff --git a/sk1/backups/backup_2026-05-19_12-37-24.sql b/sk1/backups/backup_2026-05-19_12-37-24.sql new file mode 100644 index 0000000..07e8b46 --- /dev/null +++ b/sk1/backups/backup_2026-05-19_12-37-24.sql @@ -0,0 +1,98 @@ +-- +-- PostgreSQL database dump +-- + +\restrict TBO5p6lvhgQlVM1tXKahkrIRa7cGMBX5CXBrOfkVAQUoFQjgAIX2DM2f2jG00du + +-- Dumped from database version 16.14 (Debian 16.14-1.pgdg13+1) +-- Dumped by pg_dump version 16.14 (Debian 16.14-1.pgdg13+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: transactions; Type: TABLE; Schema: public; Owner: budget_user +-- + +CREATE TABLE public.transactions ( + id integer NOT NULL, + title text NOT NULL, + amount numeric NOT NULL, + type text NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.transactions OWNER TO budget_user; + +-- +-- Name: transactions_id_seq; Type: SEQUENCE; Schema: public; Owner: budget_user +-- + +CREATE SEQUENCE public.transactions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.transactions_id_seq OWNER TO budget_user; + +-- +-- Name: transactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: budget_user +-- + +ALTER SEQUENCE public.transactions_id_seq OWNED BY public.transactions.id; + + +-- +-- Name: transactions id; Type: DEFAULT; Schema: public; Owner: budget_user +-- + +ALTER TABLE ONLY public.transactions ALTER COLUMN id SET DEFAULT nextval('public.transactions_id_seq'::regclass); + + +-- +-- Data for Name: transactions; Type: TABLE DATA; Schema: public; Owner: budget_user +-- + +COPY public.transactions (id, title, amount, type, created_at) FROM stdin; +1 asd 4 expense 2026-05-19 06:56:50.824855 +\. + + +-- +-- Name: transactions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: budget_user +-- + +SELECT pg_catalog.setval('public.transactions_id_seq', 1, true); + + +-- +-- Name: transactions transactions_pkey; Type: CONSTRAINT; Schema: public; Owner: budget_user +-- + +ALTER TABLE ONLY public.transactions + ADD CONSTRAINT transactions_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict TBO5p6lvhgQlVM1tXKahkrIRa7cGMBX5CXBrOfkVAQUoFQjgAIX2DM2f2jG00du + diff --git a/sk1/db/init.sql b/sk1/db/init.sql new file mode 100644 index 0000000..fe8a116 --- /dev/null +++ b/sk1/db/init.sql @@ -0,0 +1,7 @@ +CREATE TABLE transactions ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + amount NUMERIC NOT NULL, + type TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/sk1/docker-compose.yaml b/sk1/docker-compose.yaml new file mode 100644 index 0000000..d0e07dd --- /dev/null +++ b/sk1/docker-compose.yaml @@ -0,0 +1,50 @@ +services: + + frontend: + build: ./frontend + container_name: budget_frontend + restart: always + ports: + - "3000:3000" + depends_on: + - backend + + backend: + build: ./backend + container_name: budget_backend + restart: always + env_file: + - .env + ports: + - "5000:5000" + depends_on: + - postgres + + postgres: + image: postgres:16 + container_name: budget_postgres + restart: always + environment: + POSTGRES_USER: budget_user + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: budget_db + volumes: + - postgres_data:/var/lib/postgresql/data + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + + nginx: + image: nginx:latest + container_name: budget_nginx + restart: always + ports: + - "80:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + depends_on: + - frontend + - backend + +volumes: + postgres_data: diff --git a/sk1/frontend/Dockerfile b/sk1/frontend/Dockerfile new file mode 100644 index 0000000..c94a918 --- /dev/null +++ b/sk1/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20 + +WORKDIR /app + +COPY package.json . + +RUN npm install + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "start"] diff --git a/sk1/frontend/package.json b/sk1/frontend/package.json new file mode 100644 index 0000000..5964699 --- /dev/null +++ b/sk1/frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "budget-frontend", + "version": "1.0.0", + "private": true, + "dependencies": { + "axios": "^1.7.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-scripts": "^5.0.1" + }, + "scripts": { + "start": "react-scripts start" + } +} diff --git a/sk1/frontend/public/index.html b/sk1/frontend/public/index.html new file mode 100644 index 0000000..d71d929 --- /dev/null +++ b/sk1/frontend/public/index.html @@ -0,0 +1,10 @@ + + + + + Budget Tracker + + +
+ + diff --git a/sk1/frontend/src/App.css b/sk1/frontend/src/App.css new file mode 100644 index 0000000..451dcaf --- /dev/null +++ b/sk1/frontend/src/App.css @@ -0,0 +1,111 @@ +body { + margin: 0; + background-color: #0f172a; + font-family: Arial; +} + +.page { + min-height: 100vh; + padding: 40px; + color: white; +} + +.container { + max-width: 700px; + margin: 0 auto; +} + +.title { + text-align: center; + margin-bottom: 30px; +} + +.balance-card { + background-color: #1e293b; + padding: 30px; + border-radius: 15px; + text-align: center; + margin-bottom: 20px; +} + +.stats-container { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.income-card { + flex: 1; + background-color: #00c853; + padding: 20px; + border-radius: 15px; +} + +.expense-card { + flex: 1; + background-color: #ff1744; + padding: 20px; + border-radius: 15px; +} + +.form { + background-color: #1e293b; + padding: 20px; + border-radius: 15px; + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 20px; +} + +.input, +.select { + padding: 12px; + border-radius: 10px; + border: none; +} + +.add-button { + padding: 12px; + background-color: #2563eb; + color: white; + border: none; + border-radius: 10px; + cursor: pointer; +} + +.transactions-container { + display: flex; + flex-direction: column; + gap: 15px; +} + +.transaction-card { + background-color: #1e293b; + padding: 20px; + border-radius: 15px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.income-border { + border-left: 5px solid #00c853; +} + +.expense-border { + border-left: 5px solid #ff1744; +} + +.delete-button { + background-color: #ff1744; + color: white; + border: none; + padding: 10px 15px; + border-radius: 10px; + cursor: pointer; +} + +small { + color: #cbd5e1; +} diff --git a/sk1/frontend/src/App.js b/sk1/frontend/src/App.js new file mode 100644 index 0000000..abfc08e --- /dev/null +++ b/sk1/frontend/src/App.js @@ -0,0 +1,186 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import "./App.css"; + +function App() { + + const [transactions, setTransactions] = useState([]); + const [title, setTitle] = useState(""); + const [amount, setAmount] = useState(""); + const [type, setType] = useState("expense"); + + const fetchTransactions = async () => { + + const res = await axios.get("/api/transactions"); + + setTransactions(res.data); + }; + + useEffect(() => { + fetchTransactions(); + }, []); + + const addTransaction = async (e) => { + + e.preventDefault(); + + await axios.post("/api/transactions", { + title, + amount, + type + }); + + setTitle(""); + setAmount(""); + + fetchTransactions(); + }; + + const deleteTransaction = async (id) => { + + await axios.delete(`/api/transactions/${id}`); + + fetchTransactions(); + }; + + const income = transactions + .filter(t => t.type === "income") + .reduce((acc, t) => acc + Number(t.amount), 0); + + const expenses = transactions + .filter(t => t.type === "expense") + .reduce((acc, t) => acc + Number(t.amount), 0); + + const balance = income - expenses; + + return ( + +
+ +
+ +

+ Budget Tracker +

+ +
+ +

Balance

+ +

{balance} €

+ +
+ +
+ +
+ +

Income

+ +

{income} €

+ +
+ +
+ +

Expenses

+ +

{expenses} €

+ +
+ +
+ +
+ + setTitle(e.target.value)} + required + className="input" + /> + + setAmount(e.target.value)} + required + className="input" + /> + + + + + +
+ +
+ + {transactions.map(transaction => ( + +
+ +
+ +

{transaction.title}

+ +

{transaction.amount} €

+ + + {new Date(transaction.created_at).toLocaleString()} + + +
+ + + +
+ + ))} + +
+ +
+ +
+ ); +} + +export default App; diff --git a/sk1/frontend/src/index.js b/sk1/frontend/src/index.js new file mode 100644 index 0000000..06225be --- /dev/null +++ b/sk1/frontend/src/index.js @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +const root = ReactDOM.createRoot( + document.getElementById("root") +); + +root.render( + + + +); diff --git a/sk1/nginx/default.conf b/sk1/nginx/default.conf new file mode 100644 index 0000000..a0a3395 --- /dev/null +++ b/sk1/nginx/default.conf @@ -0,0 +1,12 @@ +server { + + listen 80; + + location / { + proxy_pass http://frontend:3000; + } + + location /api/ { + proxy_pass http://backend:5000/; + } +} diff --git a/sk1/prepare-app.sh b/sk1/prepare-app.sh new file mode 100755 index 0000000..fdee780 --- /dev/null +++ b/sk1/prepare-app.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker compose up -d --build diff --git a/sk1/remove-app.sh b/sk1/remove-app.sh new file mode 100755 index 0000000..0ec548c --- /dev/null +++ b/sk1/remove-app.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker compose down -v