adding sk1

This commit is contained in:
Ferko 2026-05-19 14:34:27 +02:00
parent 56e08dbe7b
commit 38dd167159
19 changed files with 635 additions and 0 deletions

1
sk1/.env.example Normal file
View File

@ -0,0 +1 @@
POSTGRES_PASSWORD=changeme

2
sk1/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
node_modules

13
sk1/backend/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 5000
CMD ["npm", "start"]

11
sk1/backend/db.js Normal file
View File

@ -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;

14
sk1/backend/package.json Normal file
View File

@ -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"
}
}

65
sk1/backend/server.js Normal file
View File

@ -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");
});

9
sk1/backup/backup.sh Executable file
View File

@ -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"

View File

@ -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

7
sk1/db/init.sql Normal file
View File

@ -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
);

50
sk1/docker-compose.yaml Normal file
View File

@ -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:

13
sk1/frontend/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

14
sk1/frontend/package.json Normal file
View File

@ -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"
}
}

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Budget Tracker</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

111
sk1/frontend/src/App.css Normal file
View File

@ -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;
}

186
sk1/frontend/src/App.js Normal file
View File

@ -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 (
<div className="page">
<div className="container">
<h1 className="title">
Budget Tracker
</h1>
<div className="balance-card">
<h2>Balance</h2>
<h1>{balance} </h1>
</div>
<div className="stats-container">
<div className="income-card">
<h3>Income</h3>
<p>{income} </p>
</div>
<div className="expense-card">
<h3>Expenses</h3>
<p>{expenses} </p>
</div>
</div>
<form
onSubmit={addTransaction}
className="form"
>
<input
type="text"
placeholder="transaction title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
className="input"
/>
<input
type="number"
placeholder="amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
className="input"
/>
<select
value={type}
onChange={(e) => setType(e.target.value)}
className="select"
>
<option value="expense">
expense
</option>
<option value="income">
income
</option>
</select>
<button
type="submit"
className="add-button"
>
Add Transaction
</button>
</form>
<div className="transactions-container">
{transactions.map(transaction => (
<div
key={transaction.id}
className={`transaction-card ${
transaction.type === "income"
? "income-border"
: "expense-border"
}`}
>
<div>
<h3>{transaction.title}</h3>
<p>{transaction.amount} </p>
<small>
{new Date(transaction.created_at).toLocaleString()}
</small>
</div>
<button
onClick={() => deleteTransaction(transaction.id)}
className="delete-button"
>
Delete
</button>
</div>
))}
</div>
</div>
</div>
);
}
export default App;

13
sk1/frontend/src/index.js Normal file
View File

@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);

12
sk1/nginx/default.conf Normal file
View File

@ -0,0 +1,12 @@
server {
listen 80;
location / {
proxy_pass http://frontend:3000;
}
location /api/ {
proxy_pass http://backend:5000/;
}
}

3
sk1/prepare-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
docker compose up -d --build

3
sk1/remove-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
docker compose down -v