This commit is contained in:
root 2026-03-22 13:45:44 +01:00
parent a2d6967a96
commit 4208ca4fe3
13 changed files with 2967 additions and 2966 deletions

1
z1/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.sh text eol=lf

View File

@ -1,12 +1,12 @@
FROM node:20-alpine FROM node:20-alpine
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY backend ./backend COPY backend ./backend
EXPOSE 3000 EXPOSE 3000
CMD ["node", "backend/app.js"] CMD ["node", "backend/app.js"]

View File

@ -1,226 +1,226 @@
const express = require('express'); const express = require('express');
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const session = require('express-session'); const session = require('express-session');
const path = require('path'); const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../.env') }); require('dotenv').config({ path: path.join(__dirname, '../.env') });
const app = express(); const app = express();
const FRONTEND_DIR = path.join(__dirname, '../frontend'); const FRONTEND_DIR = path.join(__dirname, '../frontend');
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST, host: process.env.DB_HOST,
port: Number(process.env.DB_PORT), port: Number(process.env.DB_PORT),
user: process.env.DB_USER, user: process.env.DB_USER,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
waitForConnections: true, waitForConnections: true,
connectionLimit: 10, connectionLimit: 10,
queueLimit: 0 queueLimit: 0
}); });
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
app.use(session({ app.use(session({
secret: process.env.SESSION_SECRET, secret: process.env.SESSION_SECRET,
resave: false, resave: false,
saveUninitialized: false saveUninitialized: false
})); }));
app.get('/', (req, res) => { app.get('/', (req, res) => {
if (req.session.user) { if (req.session.user) {
return res.redirect('/index.html'); return res.redirect('/index.html');
} }
res.sendFile(path.join(FRONTEND_DIR, 'login.html')); res.sendFile(path.join(FRONTEND_DIR, 'login.html'));
}); });
app.get('/register', (req, res) => { app.get('/register', (req, res) => {
res.sendFile(path.join(FRONTEND_DIR, 'register.html')); res.sendFile(path.join(FRONTEND_DIR, 'register.html'));
}); });
app.get('/index.html', (req, res) => { app.get('/index.html', (req, res) => {
if (!req.session.user) { if (!req.session.user) {
return res.redirect('/'); return res.redirect('/');
} }
res.sendFile(path.join(FRONTEND_DIR, 'index.html')); res.sendFile(path.join(FRONTEND_DIR, 'index.html'));
}); });
app.post('/register', async (req, res) => { app.post('/register', async (req, res) => {
const { username, password, password2 } = req.body; const { username, password, password2 } = req.body;
if (!username || !password || !password2) { if (!username || !password || !password2) {
return res.redirect('/register.html?error=' + encodeURIComponent('Vyplň všetky polia')); return res.redirect('/register.html?error=' + encodeURIComponent('Vyplň všetky polia'));
} }
if (password !== password2) { if (password !== password2) {
return res.redirect('/register.html?error=' + encodeURIComponent('Heslá sa nezhodujú')); return res.redirect('/register.html?error=' + encodeURIComponent('Heslá sa nezhodujú'));
} }
try { try {
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);
await pool.execute( await pool.execute(
'INSERT INTO users (username, password) VALUES (?, ?)', 'INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword] [username, hashedPassword]
); );
return res.redirect('/'); return res.redirect('/');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return res.redirect('/register.html?error=' + encodeURIComponent('Používateľ už existuje')); return res.redirect('/register.html?error=' + encodeURIComponent('Používateľ už existuje'));
} }
}); });
app.post('/login', async (req, res) => { app.post('/login', async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (!username || !password) { if (!username || !password) {
return res.redirect('/?error=Vyplň%20všetky%20polia'); return res.redirect('/?error=Vyplň%20všetky%20polia');
} }
try { try {
const [rows] = await pool.execute( const [rows] = await pool.execute(
'SELECT * FROM users WHERE username = ?', 'SELECT * FROM users WHERE username = ?',
[username] [username]
); );
const row = rows[0]; const row = rows[0];
if (!row) { if (!row) {
return res.redirect('/?error=Zlé%20meno%20alebo%20heslo'); return res.redirect('/?error=Zlé%20meno%20alebo%20heslo');
} }
const result = await bcrypt.compare(password, row.password); const result = await bcrypt.compare(password, row.password);
if (!result) { if (!result) {
return res.redirect('/?error=Zlé%20meno%20alebo%20heslo'); return res.redirect('/?error=Zlé%20meno%20alebo%20heslo');
} }
req.session.user = { req.session.user = {
id: row.id, id: row.id,
username: row.username username: row.username
}; };
return res.redirect('/index.html'); return res.redirect('/index.html');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return res.redirect('/?error=Chyba%20databázy'); return res.redirect('/?error=Chyba%20databázy');
} }
}); });
app.post('/logout', (req, res) => { app.post('/logout', (req, res) => {
req.session.destroy(() => { req.session.destroy(() => {
res.redirect('/'); res.redirect('/');
}); });
}); });
app.get('/api/me', (req, res) => { app.get('/api/me', (req, res) => {
if (!req.session.user) { if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' }); return res.status(401).json({ error: 'Neprihlásený používateľ' });
} }
res.json(req.session.user); res.json(req.session.user);
}); });
app.get('/api/tasks', async (req, res) => { app.get('/api/tasks', async (req, res) => {
if (!req.session.user) { if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' }); return res.status(401).json({ error: 'Neprihlásený používateľ' });
} }
try { try {
const [rows] = await pool.execute( const [rows] = await pool.execute(
'SELECT * FROM tasks WHERE user_id = ? ORDER BY deadline ASC', 'SELECT * FROM tasks WHERE user_id = ? ORDER BY deadline ASC',
[req.session.user.id] [req.session.user.id]
); );
res.json(rows); res.json(rows);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return res.status(500).json({ error: 'Chyba pri načítaní úloh' }); return res.status(500).json({ error: 'Chyba pri načítaní úloh' });
} }
}); });
app.post('/api/tasks', async (req, res) => { app.post('/api/tasks', async (req, res) => {
if (!req.session.user) { if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' }); return res.status(401).json({ error: 'Neprihlásený používateľ' });
} }
const { title, description, deadline } = req.body; const { title, description, deadline } = req.body;
if (!title || !description || !deadline) { if (!title || !description || !deadline) {
return res.status(400).json({ error: 'Vyplň všetky polia' }); return res.status(400).json({ error: 'Vyplň všetky polia' });
} }
try { try {
const [result] = await pool.execute( const [result] = await pool.execute(
'INSERT INTO tasks (user_id, title, description, deadline) VALUES (?, ?, ?, ?)', 'INSERT INTO tasks (user_id, title, description, deadline) VALUES (?, ?, ?, ?)',
[req.session.user.id, title, description, deadline] [req.session.user.id, title, description, deadline]
); );
const [rows] = await pool.execute( const [rows] = await pool.execute(
'SELECT * FROM tasks WHERE id = ?', 'SELECT * FROM tasks WHERE id = ?',
[result.insertId] [result.insertId]
); );
res.json(rows[0]); res.json(rows[0]);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return res.status(500).json({ error: 'Chyba pri ukladaní úlohy' }); return res.status(500).json({ error: 'Chyba pri ukladaní úlohy' });
} }
}); });
app.delete('/api/tasks/:id', async (req, res) => { app.delete('/api/tasks/:id', async (req, res) => {
if (!req.session.user) { if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' }); return res.status(401).json({ error: 'Neprihlásený používateľ' });
} }
try { try {
await pool.execute( await pool.execute(
'DELETE FROM tasks WHERE id = ? AND user_id = ?', 'DELETE FROM tasks WHERE id = ? AND user_id = ?',
[req.params.id, req.session.user.id] [req.params.id, req.session.user.id]
); );
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return res.status(500).json({ error: 'Chyba pri mazaní úlohy' }); return res.status(500).json({ error: 'Chyba pri mazaní úlohy' });
} }
}); });
app.patch('/api/tasks/:id/toggle', async (req, res) => { app.patch('/api/tasks/:id/toggle', async (req, res) => {
if (!req.session.user) { if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' }); return res.status(401).json({ error: 'Neprihlásený používateľ' });
} }
const { status } = req.body; const { status } = req.body;
try { try {
await pool.execute( await pool.execute(
'UPDATE tasks SET status = ? WHERE id = ? AND user_id = ?', 'UPDATE tasks SET status = ? WHERE id = ? AND user_id = ?',
[status ? 1 : 0, req.params.id, req.session.user.id] [status ? 1 : 0, req.params.id, req.session.user.id]
); );
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return res.status(500).json({ error: 'Chyba pri zmene stavu úlohy' }); return res.status(500).json({ error: 'Chyba pri zmene stavu úlohy' });
} }
}); });
app.use(express.static(FRONTEND_DIR)); app.use(express.static(FRONTEND_DIR));
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => { app.listen(PORT, async () => {
try { try {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
console.log('Pripojenie na MySQL úspešné.'); console.log('Pripojenie na MySQL úspešné.');
connection.release(); connection.release();
} catch (err) { } catch (err) {
console.error('Nepodarilo sa pripojiť na MySQL:', err.message); console.error('Nepodarilo sa pripojiť na MySQL:', err.message);
} }
console.log(`Server beží na http://localhost:${PORT}`); console.log(`Server beží na http://localhost:${PORT}`);
}); });

View File

@ -1,15 +1,15 @@
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE, username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL password VARCHAR(255) NOT NULL
); );
CREATE TABLE IF NOT EXISTS tasks ( CREATE TABLE IF NOT EXISTS tasks (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, user_id INT NOT NULL,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
deadline DATE NOT NULL, deadline DATE NOT NULL,
status TINYINT(1) NOT NULL DEFAULT 0, status TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
); );

View File

@ -1,42 +1,42 @@
services: services:
db: db:
image: mysql:8.0 image: mysql:8.0
container_name: todo_db container_name: todo_db
restart: always restart: always
environment: environment:
MYSQL_ROOT_PASSWORD: NajsilnejsieHeslo123. MYSQL_ROOT_PASSWORD: NajsilnejsieHeslo123.
MYSQL_DATABASE: zkt_zadanie MYSQL_DATABASE: zkt_zadanie
MYSQL_USER: todo_user MYSQL_USER: todo_user
MYSQL_PASSWORD: SilneHeslo123. MYSQL_PASSWORD: SilneHeslo123.
volumes: volumes:
- todo_mysql_data:/var/lib/mysql - todo_mysql_data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
ports: ports:
- "3307:3306" - "3307:3306"
backend: backend:
build: build:
context: . context: .
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
container_name: todo_backend container_name: todo_backend
restart: always restart: always
env_file: env_file:
- .env - .env
depends_on: depends_on:
- db - db
ports: ports:
- "3000:3000" - "3000:3000"
frontend: frontend:
build: build:
context: . context: .
dockerfile: frontend/Dockerfile dockerfile: frontend/Dockerfile
container_name: todo_frontend container_name: todo_frontend
restart: always restart: always
depends_on: depends_on:
- backend - backend
ports: ports:
- "8080:80" - "8080:80"
volumes: volumes:
todo_mysql_data: todo_mysql_data:

View File

@ -1,9 +1,9 @@
FROM nginx:alpine FROM nginx:alpine
COPY frontend/index.html /usr/share/nginx/html/index.html COPY frontend/index.html /usr/share/nginx/html/index.html
COPY frontend/login.html /usr/share/nginx/html/login.html COPY frontend/login.html /usr/share/nginx/html/login.html
COPY frontend/register.html /usr/share/nginx/html/register.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/generovanie_karticiek.js /usr/share/nginx/html/generovanie_karticiek.js
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80

View File

@ -1,209 +1,209 @@
const emptyMessage = document.getElementById("emptyMessage"); const emptyMessage = document.getElementById("emptyMessage");
const cardContainer = document.getElementById("cardContainer"); const cardContainer = document.getElementById("cardContainer");
const addCardBtn = document.getElementById("addCardBtn"); const addCardBtn = document.getElementById("addCardBtn");
const taskModal = document.getElementById("taskModal"); const taskModal = document.getElementById("taskModal");
const saveTaskBtn = document.getElementById("saveTaskBtn"); const saveTaskBtn = document.getElementById("saveTaskBtn");
const cancelTaskBtn = document.getElementById("cancelTaskBtn"); const cancelTaskBtn = document.getElementById("cancelTaskBtn");
const filterDate = document.getElementById("filterDate"); const filterDate = document.getElementById("filterDate");
const loggedUser = document.getElementById("loggedUser"); const loggedUser = document.getElementById("loggedUser");
let tasks = []; let tasks = [];
function formatDate(isoDate) { function formatDate(isoDate) {
if (!isoDate) return "bez termínu"; if (!isoDate) return "bez termínu";
return new Date(isoDate).toLocaleDateString("sk-SK", { return new Date(isoDate).toLocaleDateString("sk-SK", {
day: "2-digit", day: "2-digit",
month: "2-digit", month: "2-digit",
year: "numeric" year: "numeric"
}); });
} }
function updateEmptyVisibility() { function updateEmptyVisibility() {
if (tasks.length === 0) { if (tasks.length === 0) {
emptyMessage.classList.remove("hidden"); emptyMessage.classList.remove("hidden");
} else { } else {
emptyMessage.classList.add("hidden"); emptyMessage.classList.add("hidden");
} }
} }
function createCard(task) { function createCard(task) {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "p-6 rounded-xl shadow-md border hover:shadow-lg transition-all duration-200"; card.className = "p-6 rounded-xl shadow-md border hover:shadow-lg transition-all duration-200";
card.dataset.deadline = task.deadline || ""; card.dataset.deadline = task.deadline || "";
card.dataset.id = task.id; card.dataset.id = task.id;
if (task.status) { if (task.status) {
card.classList.add("bg-green-100", "border-green-200"); card.classList.add("bg-green-100", "border-green-200");
} else { } else {
card.classList.add("bg-yellow-100", "border-yellow-200"); card.classList.add("bg-yellow-100", "border-yellow-200");
} }
card.innerHTML = ` card.innerHTML = `
<h3 class="font-bold text-lg mb-2 text-gray-800">${task.title}</h3> <h3 class="font-bold text-lg mb-2 text-gray-800">${task.title}</h3>
<p class="text-gray-700 mb-4">${task.description}</p> <p class="text-gray-700 mb-4">${task.description}</p>
<p class="text-sm text-gray-600 mb-5">Termín: ${formatDate(task.deadline)}</p> <p class="text-sm text-gray-600 mb-5">Termín: ${formatDate(task.deadline)}</p>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<input type="checkbox" class="task-checkbox h-5 w-5 text-green-600 rounded border-gray-300 focus:ring-green-500" ${task.status ? "checked" : ""}> <input type="checkbox" class="task-checkbox h-5 w-5 text-green-600 rounded border-gray-300 focus:ring-green-500" ${task.status ? "checked" : ""}>
<label class="ml-2 text-sm text-gray-700 cursor-pointer">Hotovo</label> <label class="ml-2 text-sm text-gray-700 cursor-pointer">Hotovo</label>
</div> </div>
<button class="delete-btn px-5 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition"> <button class="delete-btn px-5 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition">
Zmazať Zmazať
</button> </button>
</div> </div>
`; `;
const checkbox = card.querySelector(".task-checkbox"); const checkbox = card.querySelector(".task-checkbox");
checkbox.addEventListener("change", async () => { checkbox.addEventListener("change", async () => {
const newValue = checkbox.checked ? 1 : 0; const newValue = checkbox.checked ? 1 : 0;
try { try {
const response = await fetch(`/api/tasks/${task.id}/toggle`, { const response = await fetch(`/api/tasks/${task.id}/toggle`, {
method: "PATCH", method: "PATCH",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
body: JSON.stringify({ status: newValue }) body: JSON.stringify({ status: newValue })
}); });
if (!response.ok) { if (!response.ok) {
console.error("PATCH neprešiel:", response.status); console.error("PATCH neprešiel:", response.status);
checkbox.checked = !checkbox.checked; checkbox.checked = !checkbox.checked;
return; return;
} }
task.status = newValue; task.status = newValue;
card.classList.remove( card.classList.remove(
"bg-yellow-100", "bg-yellow-100",
"border-yellow-200", "border-yellow-200",
"bg-green-100", "bg-green-100",
"border-green-200" "border-green-200"
); );
if (newValue) { if (newValue) {
card.classList.add("bg-green-100", "border-green-200"); card.classList.add("bg-green-100", "border-green-200");
} else { } else {
card.classList.add("bg-yellow-100", "border-yellow-200"); card.classList.add("bg-yellow-100", "border-yellow-200");
} }
} catch (error) { } catch (error) {
console.error("Chyba fetchu:", error); console.error("Chyba fetchu:", error);
checkbox.checked = !checkbox.checked; checkbox.checked = !checkbox.checked;
} }
}); });
const deleteBtn = card.querySelector(".delete-btn"); const deleteBtn = card.querySelector(".delete-btn");
deleteBtn.addEventListener("click", async () => { deleteBtn.addEventListener("click", async () => {
try { try {
const response = await fetch(`/api/tasks/${task.id}`, { const response = await fetch(`/api/tasks/${task.id}`, {
method: "DELETE" method: "DELETE"
}); });
if (response.ok) { if (response.ok) {
tasks = tasks.filter(t => t.id !== task.id); tasks = tasks.filter(t => t.id !== task.id);
renderTasks(); renderTasks();
} }
} catch (error) { } catch (error) {
console.error("Chyba pri mazaní úlohy:", error); console.error("Chyba pri mazaní úlohy:", error);
} }
}); });
cardContainer.appendChild(card); cardContainer.appendChild(card);
} }
function renderTasks() { function renderTasks() {
const order = filterDate.value; const order = filterDate.value;
const sortedTasks = [...tasks].sort((a, b) => { const sortedTasks = [...tasks].sort((a, b) => {
const da = a.deadline ? new Date(a.deadline) : new Date("9999-12-31"); 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"); const db = b.deadline ? new Date(b.deadline) : new Date("9999-12-31");
return order === "asc" ? da - db : db - da; return order === "asc" ? da - db : db - da;
}); });
cardContainer.innerHTML = ""; cardContainer.innerHTML = "";
sortedTasks.forEach(createCard); sortedTasks.forEach(createCard);
updateEmptyVisibility(); updateEmptyVisibility();
} }
function showModal() { function showModal() {
taskModal.classList.remove("hidden"); taskModal.classList.remove("hidden");
document.getElementById("taskTitle").value = ""; document.getElementById("taskTitle").value = "";
document.getElementById("taskDescription").value = ""; document.getElementById("taskDescription").value = "";
document.getElementById("taskDeadline").value = ""; document.getElementById("taskDeadline").value = "";
} }
function hideModal() { function hideModal() {
taskModal.classList.add("hidden"); taskModal.classList.add("hidden");
} }
async function loadUser() { async function loadUser() {
try { try {
const response = await fetch("/api/me"); const response = await fetch("/api/me");
if (!response.ok) { if (!response.ok) {
window.location.href = "/login.html"; window.location.href = "/login.html";
return; return;
} }
const user = await response.json(); const user = await response.json();
loggedUser.textContent = `Prihlásený ako: ${user.username}`; loggedUser.textContent = `Prihlásený ako: ${user.username}`;
} catch (error) { } catch (error) {
console.error("Chyba pri načítaní používateľa:", error); console.error("Chyba pri načítaní používateľa:", error);
window.location.href = "/login.html"; window.location.href = "/login.html";
} }
} }
async function loadTasks() { async function loadTasks() {
try { try {
const response = await fetch("/api/tasks"); const response = await fetch("/api/tasks");
if (!response.ok) { if (!response.ok) {
window.location.href = "/login.html"; window.location.href = "/login.html";
return; return;
} }
tasks = await response.json(); tasks = await response.json();
renderTasks(); renderTasks();
} catch (error) { } catch (error) {
console.error("Chyba pri načítaní úloh:", error); console.error("Chyba pri načítaní úloh:", error);
} }
} }
addCardBtn.addEventListener("click", showModal); addCardBtn.addEventListener("click", showModal);
cancelTaskBtn.addEventListener("click", hideModal); cancelTaskBtn.addEventListener("click", hideModal);
saveTaskBtn.addEventListener("click", async () => { saveTaskBtn.addEventListener("click", async () => {
const title = document.getElementById("taskTitle").value.trim(); const title = document.getElementById("taskTitle").value.trim();
const description = document.getElementById("taskDescription").value.trim(); const description = document.getElementById("taskDescription").value.trim();
const deadline = document.getElementById("taskDeadline").value; const deadline = document.getElementById("taskDeadline").value;
if (!title || !description || !deadline) { if (!title || !description || !deadline) {
return; return;
} }
try { try {
const response = await fetch("/api/tasks", { const response = await fetch("/api/tasks", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
body: JSON.stringify({ title, description, deadline }) body: JSON.stringify({ title, description, deadline })
}); });
if (response.ok) { if (response.ok) {
const newTask = await response.json(); const newTask = await response.json();
tasks.push(newTask); tasks.push(newTask);
renderTasks(); renderTasks();
hideModal(); hideModal();
} }
} catch (error) { } catch (error) {
console.error("Chyba pri pridávaní úlohy:", error); console.error("Chyba pri pridávaní úlohy:", error);
} }
}); });
filterDate.addEventListener("change", renderTasks); filterDate.addEventListener("change", renderTasks);
loadUser(); loadUser();
loadTasks(); loadTasks();

View File

@ -1,94 +1,94 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="sk"> <html lang="sk">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To-Do aplikácia</title> <title>To-Do aplikácia</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<style> <style>
select { select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.75rem center; background-position: right 0.75rem center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 1.25em; background-size: 1.25em;
} }
</style> </style>
</head> </head>
<body class="flex flex-col min-h-screen bg-gray-100"> <body class="flex flex-col min-h-screen bg-gray-100">
<header class="bg-orange-500 h-20 flex justify-between items-center shadow-md px-6"> <header class="bg-orange-500 h-20 flex justify-between items-center shadow-md px-6">
<h1 class="text-white font-bold text-2xl">To-Do aplikácia</h1> <h1 class="text-white font-bold text-2xl">To-Do aplikácia</h1>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<span id="loggedUser" class="text-white font-medium"></span> <span id="loggedUser" class="text-white font-medium"></span>
<form action="/logout" method="POST"> <form action="/logout" method="POST">
<button <button
type="submit" type="submit"
class="bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 rounded-lg transition" class="bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 rounded-lg transition"
> >
Odhlásiť sa Odhlásiť sa
</button> </button>
</form> </form>
</div> </div>
</header> </header>
<main class="flex-1 p-6 flex flex-col items-center bg-gray-50"> <main class="flex-1 p-6 flex flex-col items-center bg-gray-50">
<div class="w-full max-w-3xl mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="w-full max-w-3xl mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="relative"> <div class="relative">
<select id="filterDate" class="pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-lg shadow-sm focus:border-orange-400 focus:ring-orange-400 appearance-none"> <select id="filterDate" class="pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-lg shadow-sm focus:border-orange-400 focus:ring-orange-400 appearance-none">
<option value="asc">Najskôr najbližší termín</option> <option value="asc">Najskôr najbližší termín</option>
<option value="desc">Najskôr najvzdialenejší termín</option> <option value="desc">Najskôr najvzdialenejší termín</option>
</select> </select>
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
</div> </div>
<button id="addCardBtn" class="px-7 py-3.5 bg-orange-600 hover:bg-orange-700 text-white font-semibold rounded-full shadow-lg transition-all transform hover:scale-105 flex items-center gap-2"> <button id="addCardBtn" class="px-7 py-3.5 bg-orange-600 hover:bg-orange-700 text-white font-semibold rounded-full shadow-lg transition-all transform hover:scale-105 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
Pridať úlohu Pridať úlohu
</button> </button>
</div> </div>
<div id="emptyMessage" class="hidden text-center py-16 w-full max-w-3xl"> <div id="emptyMessage" class="hidden text-center py-16 w-full max-w-3xl">
<p class="text-2xl font-medium text-gray-500 mb-6">Zatiaľ žiadne úlohy</p> <p class="text-2xl font-medium text-gray-500 mb-6">Zatiaľ žiadne úlohy</p>
</div> </div>
<div id="cardContainer" class="w-full max-w-3xl space-y-5"> <div id="cardContainer" class="w-full max-w-3xl space-y-5">
</div> </div>
</main> </main>
<footer class="bg-orange-500 h-16 flex justify-center items-center text-white font-medium shadow-inner"> <footer class="bg-orange-500 h-16 flex justify-center items-center text-white font-medium shadow-inner">
<div>By Michal</div> <div>By Michal</div>
</footer> </footer>
<div id="taskModal" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center hidden z-50"> <div id="taskModal" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-xl p-8 w-full max-w-md shadow-2xl"> <div class="bg-white rounded-xl p-8 w-full max-w-md shadow-2xl">
<h2 class="text-2xl font-bold mb-6 text-gray-800">Nová úloha</h2> <h2 class="text-2xl font-bold mb-6 text-gray-800">Nová úloha</h2>
<label for="taskTitle" class="block text-sm font-medium text-gray-700 mb-1">Názov</label> <label for="taskTitle" class="block text-sm font-medium text-gray-700 mb-1">Názov</label>
<input id="taskTitle" type="text" class="w-full p-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-orange-400" placeholder="Zadajte názov úlohy"> <input id="taskTitle" type="text" class="w-full p-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-orange-400" placeholder="Zadajte názov úlohy">
<label for="taskDescription" class="block text-sm font-medium text-gray-700 mb-1">Popis</label> <label for="taskDescription" class="block text-sm font-medium text-gray-700 mb-1">Popis</label>
<textarea id="taskDescription" class="w-full p-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-orange-400" rows="4" placeholder="Detailnejší popis..."></textarea> <textarea id="taskDescription" class="w-full p-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-orange-400" rows="4" placeholder="Detailnejší popis..."></textarea>
<label for="taskDeadline" class="block text-sm font-medium text-gray-700 mb-1">Termín dokončenia</label> <label for="taskDeadline" class="block text-sm font-medium text-gray-700 mb-1">Termín dokončenia</label>
<input id="taskDeadline" type="date" class="w-full p-3 border border-gray-300 rounded-lg mb-6 focus:outline-none focus:ring-2 focus:ring-orange-400"> <input id="taskDeadline" type="date" class="w-full p-3 border border-gray-300 rounded-lg mb-6 focus:outline-none focus:ring-2 focus:ring-orange-400">
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<button id="cancelTaskBtn" class="px-5 py-2.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition">Zrušiť</button> <button id="cancelTaskBtn" class="px-5 py-2.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition">Zrušiť</button>
<button id="saveTaskBtn" class="px-5 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">Uložiť</button> <button id="saveTaskBtn" class="px-5 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">Uložiť</button>
</div> </div>
</div> </div>
</div> </div>
<script src="generovanie_karticiek.js"></script> <script src="generovanie_karticiek.js"></script>
</body> </body>
</html> </html>

View File

@ -1,77 +1,77 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="sk"> <html lang="sk">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Prihlásenie</title> <title>Prihlásenie</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
</head> </head>
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4"> <body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8"> <div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-8"> <h1 class="text-3xl font-bold text-center text-gray-800 mb-8">
Prihlásenie Prihlásenie
</h1> </h1>
<form id="loginForm" action="/login" method="POST" class="space-y-6"> <form id="loginForm" action="/login" method="POST" class="space-y-6">
<div> <div>
<label for="meno" class="block text-sm font-medium text-gray-700 mb-1"> <label for="meno" class="block text-sm font-medium text-gray-700 mb-1">
Meno / používateľ Meno / používateľ
</label> </label>
<input <input
type="text" type="text"
id="meno" id="meno"
name="username" name="username"
required required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="Zadaj svoje meno" placeholder="Zadaj svoje meno"
/> />
</div> </div>
<div> <div>
<label for="heslo" class="block text-sm font-medium text-gray-700 mb-1"> <label for="heslo" class="block text-sm font-medium text-gray-700 mb-1">
Heslo Heslo
</label> </label>
<input <input
type="password" type="password"
id="heslo" id="heslo"
name="password" name="password"
required required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="••••••••" placeholder="••••••••"
/> />
</div> </div>
<button <button
type="submit" type="submit"
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200"
> >
Prihlásiť sa Prihlásiť sa
</button> </button>
</form> </form>
<div class="text-center mt-6 text-sm text-gray-600"> <div class="text-center mt-6 text-sm text-gray-600">
Ešte nemáš účet? Ešte nemáš účet?
<a href="/register.html" class="text-orange-600 hover:underline font-medium"> <a href="/register.html" class="text-orange-600 hover:underline font-medium">
Zaregistruj sa Zaregistruj sa
</a> </a>
</div> </div>
<p id="error" class="hidden mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-center"></p> <p id="error" class="hidden mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-center"></p>
</div> </div>
<script> <script>
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const error = params.get('error'); const error = params.get('error');
const errorBox = document.getElementById('error'); const errorBox = document.getElementById('error');
if (error) { if (error) {
errorBox.textContent = error; errorBox.textContent = error;
errorBox.classList.remove('hidden'); errorBox.classList.remove('hidden');
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,42 +1,42 @@
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index login.html; index login.html;
location = / { location = / {
try_files /login.html =404; try_files /login.html =404;
} }
location /api/ { location /api/ {
proxy_pass http://backend:3000; proxy_pass http://backend:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location = /login { location = /login {
proxy_pass http://backend:3000/login; proxy_pass http://backend:3000/login;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
} }
location = /register { location = /register {
proxy_pass http://backend:3000/register; proxy_pass http://backend:3000/register;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
} }
location = /logout { location = /logout {
proxy_pass http://backend:3000/logout; proxy_pass http://backend:3000/logout;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
} }
location / { location / {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
} }
} }

View File

@ -1,91 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="sk"> <html lang="sk">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Registrácia</title> <title>Registrácia</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
</head> </head>
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4"> <body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8"> <div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-8"> <h1 class="text-3xl font-bold text-center text-gray-800 mb-8">
Registrácia Registrácia
</h1> </h1>
<form id="registerForm" action="/register" method="POST" class="space-y-6"> <form id="registerForm" action="/register" method="POST" class="space-y-6">
<div> <div>
<label for="meno" class="block text-sm font-medium text-gray-700 mb-1"> <label for="meno" class="block text-sm font-medium text-gray-700 mb-1">
Meno / používateľ Meno / používateľ
</label> </label>
<input <input
type="text" type="text"
id="meno" id="meno"
name="username" name="username"
required required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="Zadaj svoje meno" placeholder="Zadaj svoje meno"
/> />
</div> </div>
<div> <div>
<label for="heslo" class="block text-sm font-medium text-gray-700 mb-1"> <label for="heslo" class="block text-sm font-medium text-gray-700 mb-1">
Heslo Heslo
</label> </label>
<input <input
type="password" type="password"
id="heslo" id="heslo"
name="password" name="password"
required required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="••••••••" placeholder="••••••••"
/> />
</div> </div>
<div> <div>
<label for="heslo2" class="block text-sm font-medium text-gray-700 mb-1"> <label for="heslo2" class="block text-sm font-medium text-gray-700 mb-1">
Heslo znova Heslo znova
</label> </label>
<input <input
type="password" type="password"
id="heslo2" id="heslo2"
name="password2" name="password2"
required required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="Potvrď heslo" placeholder="Potvrď heslo"
/> />
</div> </div>
<button <button
type="submit" type="submit"
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200"
> >
Vytvoriť účet Vytvoriť účet
</button> </button>
</form> </form>
<div class="text-center mt-6 text-sm text-gray-600"> <div class="text-center mt-6 text-sm text-gray-600">
Už máš účet? Už máš účet?
<a href="/login.html" class="text-orange-600 hover:underline font-medium"> <a href="/login.html" class="text-orange-600 hover:underline font-medium">
Prihlás sa Prihlás sa
</a> </a>
</div> </div>
<p id="error" class="hidden mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-center"></p> <p id="error" class="hidden mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-center"></p>
</div> </div>
<script> <script>
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const error = params.get('error'); const error = params.get('error');
const errorBox = document.getElementById('error'); const errorBox = document.getElementById('error');
if (error) { if (error) {
errorBox.textContent = error; errorBox.textContent = error;
errorBox.classList.remove('hidden'); errorBox.classList.remove('hidden');
} }
</script> </script>
</body> </body>
</html> </html>

4296
z1/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
{ {
"dependencies": { "dependencies": {
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"body-parser": "^2.2.2", "body-parser": "^2.2.2",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-session": "^1.19.0", "express-session": "^1.19.0",
"mysql2": "^3.20.0", "mysql2": "^3.20.0",
"sqlite3": "^6.0.1" "sqlite3": "^6.0.1"
} }
} }