zadanie 27
This commit is contained in:
parent
49998cbc37
commit
a35bddc6cb
7
z2/.env
Normal file
7
z2/.env
Normal file
@ -0,0 +1,7 @@
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_USER=todo_user
|
||||
DB_PASSWORD=SilneHeslo123.
|
||||
DB_NAME=zkt_zadanie
|
||||
SESSION_SECRET=EsteSilnejsieHeslo123.
|
||||
PORT=3000
|
||||
1
z2/.gitattributes
vendored
Normal file
1
z2/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
23
z2/README.md
Normal file
23
z2/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
1. Na spustenie aplikácie je potrebný Docker a pre automatizáciu kontajnerizácie aj WSL prípade PC s nainštalovaným Linuxom + GIT.
|
||||
|
||||
2. Je to jednoduchá TO-DO aplikácia kde si rôzni užívatelia možu pridávať a mazať svoje pripomienky, označiť ich za hotové a filtrovať na základe dátumu.
|
||||
|
||||
3. Aplikácia využíva jeden pomenovaný zväzok todo_mysql_data, ktorý slúži na perzistetné uloženie dát z databázy.
|
||||
|
||||
4. Konfigurácia kontajnerov bola rozelená na tri samostatné služby a to frontend, backend a databáza, a sú spravované pomocou Docker compose súboru. Databázovy kontajner využíva obraz mysql, backend je postavený na obraze node:20 a frontend na nginx.
|
||||
|
||||
5. todo_backend - kontajner obsahujúci backend založený na Node.js.
|
||||
todo_db - kontajner obsahuhúci databázovú vrstvu založenú na obraze MYSQL
|
||||
todo_frontend - kotanjner obsahujúci frontend založený na HTML, Tailwind a JS. Samotný kontajner je postavný na obraze Nginx.
|
||||
|
||||
6. Ak máte systém Linux resp. WSL, stačí sa presunút na miesto do ktorého chcete naklonovať repozitár. Repozitár je verejný takže by s klonovaním nemal byť problém. Potom stačí zadať príkaz:
|
||||
|
||||
git clone git@git.kemt.fei.tuke.sk:mu590ku/zkt26.git <nazov_noveho_priecinka_do_ktoreho_sa_repozitar_naklonuje>
|
||||
|
||||
Po zadaní príkazu sa vytvorí priečinok a v ňom bude ďalší priečinok z1, ktorý už bude obsahovať ako aplikáciu tak skripty pre automatickú kontajnerizáciu. Stačí už len postupne spúšťať skripty ./prepare-app.sh, ./start-app.sh, ./stop-app.sh a ./remove-app.sh.
|
||||
|
||||
Pozn. ak by sa stalo, ze skripty nepojde spustit a vyhodi chybu typu "permission denied while trying to connect to docker deamon socket" , treba spustit skripty so superuser pravami, tj. napr. sudo ./prepare-app.sh a pod. Taktiez aplikacia vyuziva porty 8080 pre frontend, 3000 pre backend a 3307 pre databazu takze je potrebne sa uistit, ze tieto porty su volne inak sa kontajnery nezapnu
|
||||
|
||||
7. Aplikáciu si na webovom rozhraní pozriete cez port 8080, príklad: 127.0.0.1:8080
|
||||
|
||||
8. ChatGPT 5.4, StackOverflow, Reddit
|
||||
12
z2/backend/Dockerfile
Normal file
12
z2/backend/Dockerfile
Normal file
@ -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"]
|
||||
226
z2/backend/app.js
Normal file
226
z2/backend/app.js
Normal file
@ -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}`);
|
||||
});
|
||||
15
z2/db/init.sql
Normal file
15
z2/db/init.sql
Normal file
@ -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
|
||||
);
|
||||
51
z2/deployment.yml
Normal file
51
z2/deployment.yml
Normal file
@ -0,0 +1,51 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: todo-app
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backend
|
||||
namespace: todo-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: z2-backend:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: backend-env
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontend
|
||||
namespace: todo-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: z2-frontend:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
42
z2/docker-compose.yaml
Normal file
42
z2/docker-compose.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: todo_db
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: NajsilnejsieHeslo123.
|
||||
MYSQL_DATABASE: zkt_zadanie
|
||||
MYSQL_USER: todo_user
|
||||
MYSQL_PASSWORD: SilneHeslo123.
|
||||
volumes:
|
||||
- todo_mysql_data:/var/lib/mysql
|
||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "3307:3306"
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
container_name: todo_backend
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
container_name: todo_frontend
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "8080:80"
|
||||
|
||||
volumes:
|
||||
todo_mysql_data:
|
||||
9
z2/frontend/Dockerfile
Normal file
9
z2/frontend/Dockerfile
Normal file
@ -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
|
||||
209
z2/frontend/generovanie_karticiek.js
Normal file
209
z2/frontend/generovanie_karticiek.js
Normal file
@ -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 = `
|
||||
<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-sm text-gray-600 mb-5">Termín: ${formatDate(task.deadline)}</p>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<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" : ""}>
|
||||
<label class="ml-2 text-sm text-gray-700 cursor-pointer">Hotovo</label>
|
||||
</div>
|
||||
|
||||
<button class="delete-btn px-5 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition">
|
||||
Zmazať
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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();
|
||||
94
z2/frontend/index.html
Normal file
94
z2/frontend/index.html
Normal file
@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sk">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>To-Do aplikácia</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
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-position: right 0.75rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.25em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<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">
|
||||
<h1 class="text-white font-bold text-2xl">To-Do aplikácia</h1>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="loggedUser" class="text-white font-medium"></span>
|
||||
<form action="/logout" method="POST">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 rounded-lg transition"
|
||||
>
|
||||
Odhlásiť sa
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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="flex items-center gap-3">
|
||||
<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">
|
||||
<option value="asc">Najskôr najbližší termín</option>
|
||||
<option value="desc">Najskôr najvzdialenejší termín</option>
|
||||
</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">
|
||||
<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>
|
||||
</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">
|
||||
<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" />
|
||||
</svg>
|
||||
Pridať úlohu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div id="cardContainer" class="w-full max-w-3xl space-y-5">
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="bg-orange-500 h-16 flex justify-center items-center text-white font-medium shadow-inner">
|
||||
<div>By Michal</div>
|
||||
</footer>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="generovanie_karticiek.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
77
z2/frontend/login.html
Normal file
77
z2/frontend/login.html
Normal file
@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sk">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Prihlásenie</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<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">
|
||||
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-8">
|
||||
Prihlásenie
|
||||
</h1>
|
||||
|
||||
<form id="loginForm" action="/login" method="POST" class="space-y-6">
|
||||
<div>
|
||||
<label for="meno" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Meno / používateľ
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="meno"
|
||||
name="username"
|
||||
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"
|
||||
placeholder="Zadaj svoje meno"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="heslo" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Heslo
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="heslo"
|
||||
name="password"
|
||||
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"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200"
|
||||
>
|
||||
Prihlásiť sa
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-6 text-sm text-gray-600">
|
||||
Ešte nemáš účet?
|
||||
<a href="/register.html" class="text-orange-600 hover:underline font-medium">
|
||||
Zaregistruj sa
|
||||
</a>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const error = params.get('error');
|
||||
const errorBox = document.getElementById('error');
|
||||
|
||||
if (error) {
|
||||
errorBox.textContent = error;
|
||||
errorBox.classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
42
z2/frontend/nginx.conf
Normal file
42
z2/frontend/nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
91
z2/frontend/register.html
Normal file
91
z2/frontend/register.html
Normal file
@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sk">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Registrácia</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<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">
|
||||
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-8">
|
||||
Registrácia
|
||||
</h1>
|
||||
|
||||
<form id="registerForm" action="/register" method="POST" class="space-y-6">
|
||||
<div>
|
||||
<label for="meno" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Meno / používateľ
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="meno"
|
||||
name="username"
|
||||
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"
|
||||
placeholder="Zadaj svoje meno"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="heslo" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Heslo
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="heslo"
|
||||
name="password"
|
||||
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"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="heslo2" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Heslo znova
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="heslo2"
|
||||
name="password2"
|
||||
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"
|
||||
placeholder="Potvrď heslo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200"
|
||||
>
|
||||
Vytvoriť účet
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-6 text-sm text-gray-600">
|
||||
Už máš účet?
|
||||
<a href="/login.html" class="text-orange-600 hover:underline font-medium">
|
||||
Prihlás sa
|
||||
</a>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const error = params.get('error');
|
||||
const errorBox = document.getElementById('error');
|
||||
|
||||
if (error) {
|
||||
errorBox.textContent = error;
|
||||
errorBox.classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2148
z2/package-lock.json
generated
Normal file
2148
z2/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
z2/package.json
Normal file
11
z2/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"mysql2": "^3.20.0",
|
||||
"sqlite3": "^6.0.1"
|
||||
}
|
||||
}
|
||||
16
z2/prepare-app.sh
Normal file
16
z2/prepare-app.sh
Normal file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Vytvaram namespace todo-app..."
|
||||
kubectl create namespace todo-app --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Buildim backend image..."
|
||||
docker build -t z2-backend:latest -f backend/Dockerfile .
|
||||
|
||||
echo "Buildim frontend image..."
|
||||
docker build -t z2-frontend:latest -f frontend/Dockerfile .
|
||||
|
||||
echo "Vytvaram secret backend-env z .env..."
|
||||
kubectl -n todo-app delete secret backend-env --ignore-not-found
|
||||
kubectl -n todo-app create secret generic backend-env --from-env-file=.env
|
||||
|
||||
echo "Prepare hotovy."
|
||||
9
z2/remove-app.sh
Normal file
9
z2/remove-app.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
kubectl delete -f service.yml
|
||||
kubectl delete -f statefulset.yml
|
||||
kubectl delete -f deployment.yml
|
||||
kubectl delete secret backend-env -n todo-app
|
||||
kubectl delete namespace todo-app
|
||||
docker rmi z2-backend:latest
|
||||
docker rmi z2-frontend:latest
|
||||
39
z2/service.yml
Normal file
39
z2/service.yml
Normal file
@ -0,0 +1,39 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: db
|
||||
namespace: todo-app
|
||||
spec:
|
||||
selector:
|
||||
app: db
|
||||
ports:
|
||||
- port: 3306
|
||||
targetPort: 3306
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backend
|
||||
namespace: todo-app
|
||||
spec:
|
||||
selector:
|
||||
app: backend
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend
|
||||
namespace: todo-app
|
||||
spec:
|
||||
selector:
|
||||
app: frontend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
nodePort: 30080
|
||||
type: NodePort
|
||||
5
z2/start-app.sh
Normal file
5
z2/start-app.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
kubectl apply -f deployment.yml
|
||||
kubectl apply -f service.yml
|
||||
kubectl apply -f statefulset.yml
|
||||
91
z2/statefulset.yml
Normal file
91
z2/statefulset.yml
Normal file
@ -0,0 +1,91 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mysql-pv
|
||||
spec:
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
hostPath:
|
||||
path: /tmp/todo-mysql-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mysql-pvc
|
||||
namespace: todo-app
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: mysql-init-script
|
||||
namespace: todo-app
|
||||
data:
|
||||
init.sql: |
|
||||
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
|
||||
);
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: db
|
||||
namespace: todo-app
|
||||
spec:
|
||||
serviceName: db
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: db
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
value: "NajsilnejsieHeslo123."
|
||||
- name: MYSQL_DATABASE
|
||||
value: "zkt_zadanie"
|
||||
- name: MYSQL_USER
|
||||
value: "todo_user"
|
||||
- name: MYSQL_PASSWORD
|
||||
value: "SilneHeslo123."
|
||||
volumeMounts:
|
||||
- name: mysql-storage
|
||||
mountPath: /var/lib/mysql
|
||||
- name: mysql-init-volume
|
||||
mountPath: /docker-entrypoint-initdb.d/init.sql
|
||||
subPath: init.sql
|
||||
volumes:
|
||||
- name: mysql-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: mysql-pvc
|
||||
- name: mysql-init-volume
|
||||
configMap:
|
||||
name: mysql-init-script
|
||||
5
z2/stop-app.sh
Normal file
5
z2/stop-app.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
kubectl delete -f service.yml
|
||||
kubectl delete -f statefulset.yml
|
||||
kubectl delete -f deployment.yml
|
||||
Loading…
Reference in New Issue
Block a user