From 576a8058354064e98498aca921a7bbb18c44e7a7 Mon Sep 17 00:00:00 2001 From: Misko Utlak Date: Sun, 22 Mar 2026 13:29:11 +0100 Subject: [PATCH] Move project into z1 --- z1/backend/Dockerfile | 12 + z1/backend/app.js | 226 ++++++++++++++++++ z1/db/init.sql | 15 ++ docker-compose.yaml => z1/docker-compose.yaml | 0 z1/frontend/Dockerfile | 9 + z1/frontend/generovanie_karticiek.js | 209 ++++++++++++++++ z1/frontend/index.html | 94 ++++++++ z1/frontend/login.html | 77 ++++++ z1/frontend/nginx.conf | 42 ++++ z1/frontend/register.html | 91 +++++++ package-lock.json => z1/package-lock.json | 0 package.json => z1/package.json | 0 prepare-app.sh => z1/prepare-app.sh | 0 remove-app.sh => z1/remove-app.sh | 0 start-app.sh => z1/start-app.sh | 0 stop-app.sh => z1/stop-app.sh | 0 16 files changed, 775 insertions(+) create mode 100644 z1/backend/Dockerfile create mode 100644 z1/backend/app.js create mode 100644 z1/db/init.sql rename docker-compose.yaml => z1/docker-compose.yaml (100%) create mode 100644 z1/frontend/Dockerfile create mode 100644 z1/frontend/generovanie_karticiek.js create mode 100644 z1/frontend/index.html create mode 100644 z1/frontend/login.html create mode 100644 z1/frontend/nginx.conf create mode 100644 z1/frontend/register.html rename package-lock.json => z1/package-lock.json (100%) rename package.json => z1/package.json (100%) rename prepare-app.sh => z1/prepare-app.sh (100%) rename remove-app.sh => z1/remove-app.sh (100%) rename start-app.sh => z1/start-app.sh (100%) rename stop-app.sh => z1/stop-app.sh (100%) diff --git a/z1/backend/Dockerfile b/z1/backend/Dockerfile new file mode 100644 index 0000000..f665667 --- /dev/null +++ b/z1/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY backend ./backend + +EXPOSE 3000 + +CMD ["node", "backend/app.js"] \ No newline at end of file diff --git a/z1/backend/app.js b/z1/backend/app.js new file mode 100644 index 0000000..6dc0818 --- /dev/null +++ b/z1/backend/app.js @@ -0,0 +1,226 @@ +const express = require('express'); +const mysql = require('mysql2/promise'); +const bcrypt = require('bcryptjs'); +const session = require('express-session'); +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '../.env') }); + +const app = express(); + +const FRONTEND_DIR = path.join(__dirname, '../frontend'); + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); + +app.use(session({ + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false +})); + +app.get('/', (req, res) => { + if (req.session.user) { + return res.redirect('/index.html'); + } + res.sendFile(path.join(FRONTEND_DIR, 'login.html')); +}); + +app.get('/register', (req, res) => { + res.sendFile(path.join(FRONTEND_DIR, 'register.html')); +}); + +app.get('/index.html', (req, res) => { + if (!req.session.user) { + return res.redirect('/'); + } + res.sendFile(path.join(FRONTEND_DIR, 'index.html')); +}); + +app.post('/register', async (req, res) => { + const { username, password, password2 } = req.body; + + if (!username || !password || !password2) { + return res.redirect('/register.html?error=' + encodeURIComponent('Vyplň všetky polia')); + } + + if (password !== password2) { + return res.redirect('/register.html?error=' + encodeURIComponent('Heslá sa nezhodujú')); + } + + try { + const hashedPassword = await bcrypt.hash(password, 10); + + await pool.execute( + 'INSERT INTO users (username, password) VALUES (?, ?)', + [username, hashedPassword] + ); + + return res.redirect('/'); + } catch (err) { + console.error(err); + return res.redirect('/register.html?error=' + encodeURIComponent('Používateľ už existuje')); + } +}); + +app.post('/login', async (req, res) => { + const { username, password } = req.body; + + if (!username || !password) { + return res.redirect('/?error=Vyplň%20všetky%20polia'); + } + + try { + const [rows] = await pool.execute( + 'SELECT * FROM users WHERE username = ?', + [username] + ); + + const row = rows[0]; + + if (!row) { + return res.redirect('/?error=Zlé%20meno%20alebo%20heslo'); + } + + const result = await bcrypt.compare(password, row.password); + + if (!result) { + return res.redirect('/?error=Zlé%20meno%20alebo%20heslo'); + } + + req.session.user = { + id: row.id, + username: row.username + }; + + return res.redirect('/index.html'); + } catch (err) { + console.error(err); + return res.redirect('/?error=Chyba%20databázy'); + } +}); + +app.post('/logout', (req, res) => { + req.session.destroy(() => { + res.redirect('/'); + }); +}); + +app.get('/api/me', (req, res) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Neprihlásený používateľ' }); + } + + res.json(req.session.user); +}); + +app.get('/api/tasks', async (req, res) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Neprihlásený používateľ' }); + } + + try { + const [rows] = await pool.execute( + 'SELECT * FROM tasks WHERE user_id = ? ORDER BY deadline ASC', + [req.session.user.id] + ); + + res.json(rows); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'Chyba pri načítaní úloh' }); + } +}); + +app.post('/api/tasks', async (req, res) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Neprihlásený používateľ' }); + } + + const { title, description, deadline } = req.body; + + if (!title || !description || !deadline) { + return res.status(400).json({ error: 'Vyplň všetky polia' }); + } + + try { + const [result] = await pool.execute( + 'INSERT INTO tasks (user_id, title, description, deadline) VALUES (?, ?, ?, ?)', + [req.session.user.id, title, description, deadline] + ); + + const [rows] = await pool.execute( + 'SELECT * FROM tasks WHERE id = ?', + [result.insertId] + ); + + res.json(rows[0]); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'Chyba pri ukladaní úlohy' }); + } +}); + +app.delete('/api/tasks/:id', async (req, res) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Neprihlásený používateľ' }); + } + + try { + await pool.execute( + 'DELETE FROM tasks WHERE id = ? AND user_id = ?', + [req.params.id, req.session.user.id] + ); + + res.json({ success: true }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'Chyba pri mazaní úlohy' }); + } +}); + +app.patch('/api/tasks/:id/toggle', async (req, res) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Neprihlásený používateľ' }); + } + + const { status } = req.body; + + try { + await pool.execute( + 'UPDATE tasks SET status = ? WHERE id = ? AND user_id = ?', + [status ? 1 : 0, req.params.id, req.session.user.id] + ); + + res.json({ success: true }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'Chyba pri zmene stavu úlohy' }); + } +}); + +app.use(express.static(FRONTEND_DIR)); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, async () => { + try { + const connection = await pool.getConnection(); + console.log('Pripojenie na MySQL úspešné.'); + connection.release(); + } catch (err) { + console.error('Nepodarilo sa pripojiť na MySQL:', err.message); + } + + console.log(`Server beží na http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/z1/db/init.sql b/z1/db/init.sql new file mode 100644 index 0000000..36f2831 --- /dev/null +++ b/z1/db/init.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + deadline DATE NOT NULL, + status TINYINT(1) NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/docker-compose.yaml b/z1/docker-compose.yaml similarity index 100% rename from docker-compose.yaml rename to z1/docker-compose.yaml diff --git a/z1/frontend/Dockerfile b/z1/frontend/Dockerfile new file mode 100644 index 0000000..dce60f2 --- /dev/null +++ b/z1/frontend/Dockerfile @@ -0,0 +1,9 @@ +FROM nginx:alpine + +COPY frontend/index.html /usr/share/nginx/html/index.html +COPY frontend/login.html /usr/share/nginx/html/login.html +COPY frontend/register.html /usr/share/nginx/html/register.html +COPY frontend/generovanie_karticiek.js /usr/share/nginx/html/generovanie_karticiek.js +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 \ No newline at end of file diff --git a/z1/frontend/generovanie_karticiek.js b/z1/frontend/generovanie_karticiek.js new file mode 100644 index 0000000..4a5c61c --- /dev/null +++ b/z1/frontend/generovanie_karticiek.js @@ -0,0 +1,209 @@ +const emptyMessage = document.getElementById("emptyMessage"); +const cardContainer = document.getElementById("cardContainer"); +const addCardBtn = document.getElementById("addCardBtn"); +const taskModal = document.getElementById("taskModal"); +const saveTaskBtn = document.getElementById("saveTaskBtn"); +const cancelTaskBtn = document.getElementById("cancelTaskBtn"); +const filterDate = document.getElementById("filterDate"); +const loggedUser = document.getElementById("loggedUser"); + +let tasks = []; + +function formatDate(isoDate) { + if (!isoDate) return "bez termínu"; + return new Date(isoDate).toLocaleDateString("sk-SK", { + day: "2-digit", + month: "2-digit", + year: "numeric" + }); +} + +function updateEmptyVisibility() { + if (tasks.length === 0) { + emptyMessage.classList.remove("hidden"); + } else { + emptyMessage.classList.add("hidden"); + } +} + +function createCard(task) { + const card = document.createElement("div"); + card.className = "p-6 rounded-xl shadow-md border hover:shadow-lg transition-all duration-200"; + card.dataset.deadline = task.deadline || ""; + card.dataset.id = task.id; + + if (task.status) { + card.classList.add("bg-green-100", "border-green-200"); + } else { + card.classList.add("bg-yellow-100", "border-yellow-200"); + } + + card.innerHTML = ` +

${task.title}

+

${task.description}

+

Termín: ${formatDate(task.deadline)}

+ +
+
+ + +
+ + +
+ `; + + const checkbox = card.querySelector(".task-checkbox"); + checkbox.addEventListener("change", async () => { + const newValue = checkbox.checked ? 1 : 0; + + try { + const response = await fetch(`/api/tasks/${task.id}/toggle`, { + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ status: newValue }) + }); + + if (!response.ok) { + console.error("PATCH neprešiel:", response.status); + checkbox.checked = !checkbox.checked; + return; + } + + task.status = newValue; + + card.classList.remove( + "bg-yellow-100", + "border-yellow-200", + "bg-green-100", + "border-green-200" + ); + + if (newValue) { + card.classList.add("bg-green-100", "border-green-200"); + } else { + card.classList.add("bg-yellow-100", "border-yellow-200"); + } + } catch (error) { + console.error("Chyba fetchu:", error); + checkbox.checked = !checkbox.checked; + } + }); + + const deleteBtn = card.querySelector(".delete-btn"); + deleteBtn.addEventListener("click", async () => { + try { + const response = await fetch(`/api/tasks/${task.id}`, { + method: "DELETE" + }); + + if (response.ok) { + tasks = tasks.filter(t => t.id !== task.id); + renderTasks(); + } + } catch (error) { + console.error("Chyba pri mazaní úlohy:", error); + } + }); + + cardContainer.appendChild(card); +} + +function renderTasks() { + const order = filterDate.value; + + const sortedTasks = [...tasks].sort((a, b) => { + const da = a.deadline ? new Date(a.deadline) : new Date("9999-12-31"); + const db = b.deadline ? new Date(b.deadline) : new Date("9999-12-31"); + return order === "asc" ? da - db : db - da; + }); + + cardContainer.innerHTML = ""; + sortedTasks.forEach(createCard); + updateEmptyVisibility(); +} + +function showModal() { + taskModal.classList.remove("hidden"); + document.getElementById("taskTitle").value = ""; + document.getElementById("taskDescription").value = ""; + document.getElementById("taskDeadline").value = ""; +} + +function hideModal() { + taskModal.classList.add("hidden"); +} + +async function loadUser() { + try { + const response = await fetch("/api/me"); + + if (!response.ok) { + window.location.href = "/login.html"; + return; + } + + const user = await response.json(); + loggedUser.textContent = `Prihlásený ako: ${user.username}`; + } catch (error) { + console.error("Chyba pri načítaní používateľa:", error); + window.location.href = "/login.html"; + } +} + +async function loadTasks() { + try { + const response = await fetch("/api/tasks"); + + if (!response.ok) { + window.location.href = "/login.html"; + return; + } + + tasks = await response.json(); + renderTasks(); + } catch (error) { + console.error("Chyba pri načítaní úloh:", error); + } +} + +addCardBtn.addEventListener("click", showModal); +cancelTaskBtn.addEventListener("click", hideModal); + +saveTaskBtn.addEventListener("click", async () => { + const title = document.getElementById("taskTitle").value.trim(); + const description = document.getElementById("taskDescription").value.trim(); + const deadline = document.getElementById("taskDeadline").value; + + if (!title || !description || !deadline) { + return; + } + + try { + const response = await fetch("/api/tasks", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ title, description, deadline }) + }); + + if (response.ok) { + const newTask = await response.json(); + tasks.push(newTask); + renderTasks(); + hideModal(); + } + } catch (error) { + console.error("Chyba pri pridávaní úlohy:", error); + } +}); + +filterDate.addEventListener("change", renderTasks); + +loadUser(); +loadTasks(); \ No newline at end of file diff --git a/z1/frontend/index.html b/z1/frontend/index.html new file mode 100644 index 0000000..e178b13 --- /dev/null +++ b/z1/frontend/index.html @@ -0,0 +1,94 @@ + + + + + + To-Do aplikácia + + + + + +
+

To-Do aplikácia

+ +
+ +
+ +
+
+
+ +
+ +
+ +
+
+ + + + +
+
+ + +
+ + + +
+
+ +
+ + + + + + + + \ No newline at end of file diff --git a/z1/frontend/login.html b/z1/frontend/login.html new file mode 100644 index 0000000..924b54b --- /dev/null +++ b/z1/frontend/login.html @@ -0,0 +1,77 @@ + + + + + + Prihlásenie + + + + +
+ +

+ Prihlásenie +

+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+ Ešte nemáš účet? + + Zaregistruj sa + +
+ + + +
+ + + + + \ No newline at end of file diff --git a/z1/frontend/nginx.conf b/z1/frontend/nginx.conf new file mode 100644 index 0000000..e47745a --- /dev/null +++ b/z1/frontend/nginx.conf @@ -0,0 +1,42 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index login.html; + + location = / { + try_files /login.html =404; + } + + location /api/ { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /login { + proxy_pass http://backend:3000/login; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location = /register { + proxy_pass http://backend:3000/register; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location = /logout { + proxy_pass http://backend:3000/logout; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location / { + try_files $uri $uri/ =404; + } +} \ No newline at end of file diff --git a/z1/frontend/register.html b/z1/frontend/register.html new file mode 100644 index 0000000..a1b01f5 --- /dev/null +++ b/z1/frontend/register.html @@ -0,0 +1,91 @@ + + + + + + Registrácia + + + + +
+ +

+ Registrácia +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ Už máš účet? + + Prihlás sa + +
+ + + +
+ + + + + \ No newline at end of file diff --git a/package-lock.json b/z1/package-lock.json similarity index 100% rename from package-lock.json rename to z1/package-lock.json diff --git a/package.json b/z1/package.json similarity index 100% rename from package.json rename to z1/package.json diff --git a/prepare-app.sh b/z1/prepare-app.sh similarity index 100% rename from prepare-app.sh rename to z1/prepare-app.sh diff --git a/remove-app.sh b/z1/remove-app.sh similarity index 100% rename from remove-app.sh rename to z1/remove-app.sh diff --git a/start-app.sh b/z1/start-app.sh similarity index 100% rename from start-app.sh rename to z1/start-app.sh diff --git a/stop-app.sh b/z1/stop-app.sh similarity index 100% rename from stop-app.sh rename to z1/stop-app.sh