school-timetracker/frontend/public/index.html
Patryk Hegenberg 3ac1947106 feat: improve app security and error handling
Improve overall app security by:
- using dynamic statements for all sql querries
- introducing environment variables for initial admin password
- introducing enironment variable for cors address
- improving error handling
2025-11-09 12:13:47 +01:00

338 lines
8.4 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Zeiterfassung</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<style>
/* Toast-Container */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 12px;
max-width: 400px;
pointer-events: none;
}
/* Basis-Toast */
.toast {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
pointer-events: all;
min-width: 320px;
transition: all 0.3s ease;
border-left: 4px solid;
}
.toast:hover {
transform: translateX(-5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
/* Toast-Content */
.toast-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.toast-icon {
font-size: 1.25rem;
display: flex;
align-items: center;
}
.toast-message {
font-size: 0.95rem;
line-height: 1.4;
color: #2c3e50;
font-weight: 500;
}
/* Close-Button */
.toast-close {
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
margin-left: 12px;
color: rgba(0, 0, 0, 0.4);
transition: color 0.2s ease;
font-size: 1rem;
}
.toast-close:hover {
color: rgba(0, 0, 0, 0.7);
}
/* Toast-Typen */
.toast-error {
background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
border-left-color: #e53e3e;
}
.toast-error .toast-icon {
color: #e53e3e;
}
.toast-success {
background: linear-gradient(135deg, #f0fff4 0%, #e6ffed 100%);
border-left-color: #38a169;
}
.toast-success .toast-icon {
color: #38a169;
}
.toast-info {
background: linear-gradient(135deg, #ebf8ff 0%, #e0f3ff 100%);
border-left-color: #3182ce;
}
.toast-info .toast-icon {
color: #3182ce;
}
.toast-warning {
background: linear-gradient(135deg, #fffaf0 0%, #fff5e6 100%);
border-left-color: #dd6b20;
}
.toast-warning .toast-icon {
color: #dd6b20;
}
/* Animationen */
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.toast.dismissing {
animation: slideOut 0.3s ease-in forwards;
}
/* Mobile Anpassungen */
@media screen and (max-width: 768px) {
.toast-container {
top: 10px;
right: 10px;
left: 10px;
max-width: none;
}
.toast {
min-width: auto;
width: 100%;
}
.toast-message {
font-size: 0.9rem;
}
}
/* Dark Mode Support (optional) */
@media (prefers-color-scheme: dark) {
.toast {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.toast-message {
color: #1a202c;
}
.toast-close {
color: rgba(0, 0, 0, 0.5);
}
.toast-close:hover {
color: rgba(0, 0, 0, 0.8);
}
}
body {
min-height: 100vh;
}
.table-container {
overflow-x: auto;
}
@media screen and (max-width: 768px) {
.level {
flex-direction: column;
}
.level-left,
.level-right {
width: 100%;
}
.level-item {
justify-content: center;
margin-bottom: 0.5rem;
}
.buttons {
flex-wrap: wrap;
}
.button {
margin-bottom: 0.5rem;
}
}
.fa-spinner {
animation: fa-spin 1s infinite linear;
}
@keyframes fa-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="elm"></div>
<script src="/elm.js"></script>
<script>
function getStoredData() {
try {
const data = localStorage.getItem("timetracking");
if (data) {
return JSON.parse(data);
}
} catch (e) {
console.error("Failed to parse stored data:", e);
}
return {token: null, isAdmin: false};
}
function saveData(token, isAdmin) {
try {
localStorage.setItem(
"timetracking",
JSON.stringify({
token: token,
isAdmin: isAdmin,
}),
);
} catch (e) {
console.error("Failed to save data:", e);
}
}
function clearData() {
try {
localStorage.removeItem("timetracking");
} catch (e) {
console.error("Failed to clear data:", e);
}
}
const storedData = getStoredData();
const app = Elm.Main.init({
node: document.getElementById("elm"),
flags: {
token: storedData.token,
isAdmin: storedData.isAdmin,
},
});
app.ports.saveToken.subscribe(function (data) {
saveData(data.token, data.isAdmin);
});
app.ports.removeToken.subscribe(function () {
clearData();
});
app.ports.confirmDelete.subscribe(function (message) {
const confirmed = confirm(message);
app.ports.confirmDeleteResponse.send(confirmed);
});
document.addEventListener("DOMContentLoaded", () => {
function setupBurgerMenu() {
const burgers = document.querySelectorAll(".navbar-burger");
burgers.forEach((burger) => {
burger.addEventListener("click", () => {
const target = burger.dataset.target;
const menu = document.getElementById(target);
if (menu) {
burger.classList.toggle("is-active");
menu.classList.toggle("is-active");
}
});
});
}
setupBurgerMenu();
const observer = new MutationObserver((mutations) => {
setupBurgerMenu();
});
observer.observe(document.getElementById("elm"), {
childList: true,
subtree: true,
});
});
if (
"serviceWorker" in navigator &&
window.location.protocol === "https:"
) {
navigator.serviceWorker.register("/sw.js").catch(() => {
console.log("Service Worker registration failed");
});
}
</script>
</body>
</html>