adding sk1
This commit is contained in:
parent
56e08dbe7b
commit
38dd167159
1
sk1/.env.example
Normal file
1
sk1/.env.example
Normal file
@ -0,0 +1 @@
|
||||
POSTGRES_PASSWORD=changeme
|
||||
2
sk1/.gitignore
vendored
Normal file
2
sk1/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.env
|
||||
node_modules
|
||||
13
sk1/backend/Dockerfile
Normal file
13
sk1/backend/Dockerfile
Normal 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
11
sk1/backend/db.js
Normal 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
14
sk1/backend/package.json
Normal 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
65
sk1/backend/server.js
Normal 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
9
sk1/backup/backup.sh
Executable 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"
|
||||
98
sk1/backups/backup_2026-05-19_12-37-24.sql
Normal file
98
sk1/backups/backup_2026-05-19_12-37-24.sql
Normal 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
7
sk1/db/init.sql
Normal 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
50
sk1/docker-compose.yaml
Normal 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
13
sk1/frontend/Dockerfile
Normal 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
14
sk1/frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
sk1/frontend/public/index.html
Normal file
10
sk1/frontend/public/index.html
Normal 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
111
sk1/frontend/src/App.css
Normal 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
186
sk1/frontend/src/App.js
Normal 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
13
sk1/frontend/src/index.js
Normal 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
12
sk1/nginx/default.conf
Normal 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
3
sk1/prepare-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker compose up -d --build
|
||||
3
sk1/remove-app.sh
Executable file
3
sk1/remove-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker compose down -v
|
||||
Loading…
Reference in New Issue
Block a user