523 lines
16 KiB
JavaScript
523 lines
16 KiB
JavaScript
/**
|
||
* app.js – Haupteinstiegspunkt der Encore-Webanwendung.
|
||
* Koordiniert Authentifizierung (via Modal), Navigation, Suche,
|
||
* gespeicherte Events und Einladungen.
|
||
*/
|
||
|
||
import { getEvents } from "./services/eventService.js";
|
||
import { renderEventList } from "./ui/eventList.js";
|
||
import { getFilters } from "./ui/filters.js";
|
||
import { login, register } from "./auth.js";
|
||
import { createEventCard } from "./ui/eventCard.js";
|
||
|
||
// =========================
|
||
// GLOBALER ZUSTAND
|
||
// window-Properties damit eventCard.js auf den Nutzer zugreifen kann
|
||
// =========================
|
||
let currentUser = null;
|
||
let currentPassword = null;
|
||
let lastRenderedEvents = [];
|
||
window.currentUser = null;
|
||
window.currentPassword = null;
|
||
|
||
// =========================
|
||
// DOM – HEADER AUTH
|
||
// =========================
|
||
const loginBtn = document.querySelector("#login-btn");
|
||
const registerBtn = document.querySelector("#register-btn");
|
||
const authArea = document.querySelector("#auth-area");
|
||
const userArea = document.querySelector("#user-area");
|
||
const userNameEl = document.querySelector("#user-name");
|
||
const logoutBtn = document.querySelector("#logout-btn");
|
||
|
||
// =========================
|
||
// DOM – AUTH MODAL
|
||
// =========================
|
||
const authModalEl = document.querySelector("#auth-modal");
|
||
const bsModal = new bootstrap.Modal(authModalEl);
|
||
const loginTabBtn = document.querySelector("#login-tab-btn");
|
||
const registerTabBtn = document.querySelector("#register-tab-btn");
|
||
const modalLoginBtn = document.querySelector("#modal-login-btn");
|
||
const modalRegBtn = document.querySelector("#modal-register-btn");
|
||
const modalUsername = document.querySelector("#modal-username");
|
||
const modalPassword = document.querySelector("#modal-password");
|
||
const modalRegUsername = document.querySelector("#modal-reg-username");
|
||
const loginError = document.querySelector("#login-error");
|
||
const registerError = document.querySelector("#register-error");
|
||
const registerSuccess = document.querySelector("#register-success");
|
||
const regPasswordEl = document.querySelector("#reg-password");
|
||
|
||
// =========================
|
||
// DOM – NAVIGATION
|
||
// =========================
|
||
const navEvents = document.querySelector("#nav-events");
|
||
const navSaved = document.querySelector("#nav-my-events");
|
||
const navInv = document.querySelector("#nav-invitations");
|
||
|
||
// =========================
|
||
// DOM – SEKTIONEN
|
||
// =========================
|
||
const searchSection = document.querySelector("#search-section");
|
||
const eventsSection = document.querySelector("#events-section");
|
||
const savedSection = document.querySelector("#saved-section");
|
||
const invSection = document.querySelector("#invitations-section");
|
||
|
||
// =========================
|
||
// MODAL ÖFFNEN – Korrekte Tab-Vorauswahl
|
||
// =========================
|
||
loginBtn.addEventListener("click", () => {
|
||
bootstrap.Tab.getOrCreateInstance(loginTabBtn).show();
|
||
bsModal.show();
|
||
});
|
||
|
||
registerBtn.addEventListener("click", () => {
|
||
bootstrap.Tab.getOrCreateInstance(registerTabBtn).show();
|
||
bsModal.show();
|
||
});
|
||
|
||
// Modal-Felder zurücksetzen wenn geschlossen
|
||
authModalEl.addEventListener("hidden.bs.modal", () => {
|
||
modalUsername.value = "";
|
||
modalPassword.value = "";
|
||
modalRegUsername.value = "";
|
||
loginError.hidden = true;
|
||
registerError.hidden = true;
|
||
registerSuccess.classList.add("d-none");
|
||
modalRegBtn.textContent = "Konto erstellen";
|
||
modalRegBtn.disabled = false;
|
||
lastRegisteredUser = null;
|
||
});
|
||
|
||
// =========================
|
||
// LOGIN VIA MODAL
|
||
// =========================
|
||
modalLoginBtn.addEventListener("click", async () => {
|
||
const username = modalUsername.value.trim();
|
||
const password = modalPassword.value;
|
||
|
||
loginError.hidden = true;
|
||
|
||
if (!username || !password) {
|
||
loginError.textContent = "Bitte Benutzername und Passwort eingeben.";
|
||
loginError.hidden = false;
|
||
return;
|
||
}
|
||
|
||
modalLoginBtn.disabled = true;
|
||
modalLoginBtn.textContent = "Anmelden...";
|
||
|
||
try {
|
||
const success = await login(username, password);
|
||
|
||
if (success) {
|
||
currentUser = username;
|
||
currentPassword = password;
|
||
window.currentUser = username;
|
||
window.currentPassword = password;
|
||
|
||
bsModal.hide();
|
||
|
||
authArea.classList.add("d-none");
|
||
userArea.classList.remove("d-none");
|
||
userNameEl.textContent = `👤 ${username}`;
|
||
|
||
navEvents.classList.remove("d-none");
|
||
navSaved.classList.remove("d-none");
|
||
navInv.classList.remove("d-none");
|
||
|
||
refreshVisibleEventCards();
|
||
} else {
|
||
loginError.textContent = "Login fehlgeschlagen. Bitte Zugangsdaten prüfen.";
|
||
loginError.hidden = false;
|
||
}
|
||
} catch (err) {
|
||
loginError.textContent = "Verbindungsfehler. Ist der Server erreichbar?";
|
||
loginError.hidden = false;
|
||
} finally {
|
||
modalLoginBtn.disabled = false;
|
||
modalLoginBtn.textContent = "Anmelden";
|
||
}
|
||
});
|
||
|
||
// Enter in Passwort-Feld löst Login aus
|
||
modalPassword.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") modalLoginBtn.click();
|
||
});
|
||
|
||
// =========================
|
||
// REGISTRIERUNG VIA MODAL
|
||
// Zustandsvariable: nach Erfolg wechselt Button zu "Zum Login"
|
||
// =========================
|
||
let lastRegisteredUser = null;
|
||
|
||
modalRegBtn.addEventListener("click", async () => {
|
||
// Nach erfolgreicher Registrierung → zu Login-Tab wechseln und Benutzernamen vorfüllen
|
||
if (lastRegisteredUser) {
|
||
modalUsername.value = lastRegisteredUser;
|
||
lastRegisteredUser = null;
|
||
bootstrap.Tab.getOrCreateInstance(loginTabBtn).show();
|
||
return;
|
||
}
|
||
|
||
const username = modalRegUsername.value.trim();
|
||
registerError.hidden = true;
|
||
|
||
if (!username) {
|
||
registerError.textContent = "Bitte einen Benutzernamen eingeben.";
|
||
registerError.hidden = false;
|
||
return;
|
||
}
|
||
|
||
modalRegBtn.disabled = true;
|
||
modalRegBtn.textContent = "Erstelle Konto...";
|
||
|
||
try {
|
||
const data = await register(username);
|
||
|
||
if (data.password) {
|
||
lastRegisteredUser = username;
|
||
regPasswordEl.textContent = data.password;
|
||
registerSuccess.classList.remove("d-none");
|
||
modalRegBtn.disabled = false;
|
||
modalRegBtn.textContent = "Zum Login →";
|
||
} else {
|
||
registerError.textContent = data.message || "Registrierung fehlgeschlagen.";
|
||
registerError.hidden = false;
|
||
modalRegBtn.disabled = false;
|
||
modalRegBtn.textContent = "Konto erstellen";
|
||
}
|
||
} catch (err) {
|
||
registerError.textContent = "Verbindungsfehler bei der Registrierung.";
|
||
registerError.hidden = false;
|
||
modalRegBtn.disabled = false;
|
||
modalRegBtn.textContent = "Konto erstellen";
|
||
}
|
||
});
|
||
|
||
// =========================
|
||
// LOGOUT
|
||
// =========================
|
||
logoutBtn.addEventListener("click", () => {
|
||
currentUser = null;
|
||
currentPassword = null;
|
||
window.currentUser = null;
|
||
window.currentPassword = null;
|
||
|
||
authArea.classList.remove("d-none");
|
||
userArea.classList.add("d-none");
|
||
navEvents.classList.add("d-none");
|
||
navSaved.classList.add("d-none");
|
||
navInv.classList.add("d-none");
|
||
|
||
showSection("events");
|
||
refreshVisibleEventCards();
|
||
});
|
||
|
||
// =========================
|
||
// SUCHE
|
||
// =========================
|
||
const searchBtn = document.querySelector("#load-events");
|
||
const eventListEl = document.querySelector("#event-list");
|
||
const cityInput = document.querySelector("#city-input");
|
||
|
||
searchBtn.addEventListener("click", handleSearch);
|
||
|
||
cityInput.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") handleSearch();
|
||
});
|
||
|
||
async function handleSearch() {
|
||
const { city, dateFrom, dateTo, category } = getFilters();
|
||
|
||
if (!city) {
|
||
showError(eventListEl, "Bitte eine Stadt eingeben.");
|
||
return;
|
||
}
|
||
|
||
showSkeletons(eventListEl);
|
||
|
||
try {
|
||
const events = await getEvents(city);
|
||
const filtered = applyFilters(events, dateFrom, dateTo, category);
|
||
lastRenderedEvents = filtered;
|
||
renderEventList(filtered, eventListEl);
|
||
} catch (err) {
|
||
showError(eventListEl, "Fehler beim Laden der Events. Bitte erneut versuchen.");
|
||
}
|
||
}
|
||
|
||
/** Zeigt Skeleton-Platzhalterkarten während Events geladen werden. */
|
||
function showSkeletons(container, count = 6) {
|
||
container.innerHTML = "";
|
||
for (let i = 0; i < count; i++) {
|
||
const skeleton = document.createElement("div");
|
||
skeleton.className = "skeleton-card";
|
||
skeleton.setAttribute("aria-hidden", "true");
|
||
container.appendChild(skeleton);
|
||
}
|
||
}
|
||
|
||
// =========================
|
||
// CLIENT-SEITIGE FILTER
|
||
// =========================
|
||
function applyFilters(events, dateFrom, dateTo, category) {
|
||
return events.filter(event => {
|
||
const matchDateFrom = dateFrom ? event.date >= dateFrom : true;
|
||
const matchDateTo = dateTo ? event.date <= dateTo : true;
|
||
const matchCategory = category
|
||
? event.category && event.category.toLowerCase().includes(category.toLowerCase())
|
||
: true;
|
||
return matchDateFrom && matchDateTo && matchCategory;
|
||
});
|
||
}
|
||
|
||
// =========================
|
||
// NAVIGATION
|
||
// =========================
|
||
navEvents.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
showSection("events");
|
||
});
|
||
|
||
navSaved.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
showSection("saved");
|
||
loadSavedEvents();
|
||
});
|
||
|
||
navInv.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
showSection("invitations");
|
||
loadInvitations();
|
||
});
|
||
|
||
|
||
function refreshVisibleEventCards() {
|
||
if (!eventsSection.classList.contains("d-none") && lastRenderedEvents.length > 0) {
|
||
renderEventList(lastRenderedEvents, eventListEl);
|
||
}
|
||
}
|
||
|
||
function showSection(section) {
|
||
searchSection.classList.add("d-none");
|
||
eventsSection.classList.add("d-none");
|
||
savedSection.classList.add("d-none");
|
||
invSection.classList.add("d-none");
|
||
|
||
if (section === "events") {
|
||
searchSection.classList.remove("d-none");
|
||
eventsSection.classList.remove("d-none");
|
||
} else if (section === "saved") {
|
||
savedSection.classList.remove("d-none");
|
||
} else if (section === "invitations") {
|
||
invSection.classList.remove("d-none");
|
||
}
|
||
}
|
||
|
||
// =========================
|
||
// GESPEICHERTE EVENTS (MY EVENTS)
|
||
// =========================
|
||
function loadSavedEvents() {
|
||
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
||
const container = document.querySelector("#saved-list");
|
||
container.innerHTML = "";
|
||
|
||
if (saved.length === 0) {
|
||
const p = document.createElement("p");
|
||
p.className = "text-muted";
|
||
p.textContent = "Noch keine gespeicherten Events.";
|
||
container.appendChild(p);
|
||
return;
|
||
}
|
||
|
||
saved.forEach(event => {
|
||
const card = createEventCard(event);
|
||
|
||
const removeBtn = document.createElement("button");
|
||
removeBtn.type = "button";
|
||
removeBtn.textContent = "Entfernen";
|
||
removeBtn.className = "btn btn-outline-danger btn-sm mt-2";
|
||
removeBtn.addEventListener("click", () => {
|
||
removeSavedEvent(event.id);
|
||
loadSavedEvents();
|
||
});
|
||
|
||
card.appendChild(removeBtn);
|
||
container.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function removeSavedEvent(eventId) {
|
||
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
||
const updated = saved.filter(e => e.id !== eventId);
|
||
localStorage.setItem("savedEvents", JSON.stringify(updated));
|
||
}
|
||
|
||
// =========================
|
||
// EINLADUNGEN
|
||
// =========================
|
||
async function loadInvitations() {
|
||
if (!currentUser) return;
|
||
|
||
const container = document.querySelector("#invitation-list");
|
||
container.innerHTML = "<p class='text-muted'>Einladungen werden geladen...</p>";
|
||
|
||
try {
|
||
const res = await fetch("http://localhost:3000/api/invitation", {
|
||
headers: { "X-Username": currentUser }
|
||
});
|
||
|
||
if (!res.ok) throw new Error(`Server antwortete mit Status ${res.status}`);
|
||
|
||
const data = await res.json();
|
||
container.innerHTML = "";
|
||
|
||
if (data.length === 0) {
|
||
const p = document.createElement("p");
|
||
p.className = "text-muted";
|
||
p.textContent = "Keine Einladungen vorhanden.";
|
||
container.appendChild(p);
|
||
return;
|
||
}
|
||
|
||
data.forEach(inv => renderInvitation(inv, container));
|
||
|
||
} catch (err) {
|
||
showError(container, "Fehler beim Laden der Einladungen.");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rendert eine Einladungskarte – ausschliesslich DOM-Methoden (kein innerHTML mit Nutzerdaten).
|
||
* Jede Karte hat einen Löschen-Button (demonstriert HTTP DELETE).
|
||
*/
|
||
function renderInvitation(inv, container) {
|
||
const card = document.createElement("div");
|
||
card.className = "invitation-card";
|
||
|
||
const text = document.createElement("p");
|
||
const strong = document.createElement("strong");
|
||
const em = document.createElement("em");
|
||
strong.textContent = inv.fromUser;
|
||
em.textContent = inv.eventName;
|
||
text.append(strong, " hat dich zu ", em, " eingeladen.");
|
||
card.appendChild(text);
|
||
|
||
if (inv.eventUrl) {
|
||
const link = document.createElement("a");
|
||
link.href = inv.eventUrl;
|
||
link.target = "_blank";
|
||
link.rel = "noopener noreferrer";
|
||
link.className = "btn btn-outline-primary btn-sm mb-2";
|
||
link.textContent = "Event öffnen";
|
||
card.appendChild(link);
|
||
}
|
||
|
||
if (inv.status !== "pending") {
|
||
const status = document.createElement("p");
|
||
status.className = inv.status === "accepted" ? "text-success" : "text-muted";
|
||
status.textContent = inv.status === "accepted" ? "✓ Angenommen" : "Abgelehnt";
|
||
card.appendChild(status);
|
||
card.appendChild(buildDeleteBtn(inv.id, card));
|
||
container.appendChild(card);
|
||
return;
|
||
}
|
||
|
||
const btnGroup = document.createElement("div");
|
||
btnGroup.className = "d-flex gap-2 flex-wrap";
|
||
|
||
const acceptBtn = document.createElement("button");
|
||
acceptBtn.type = "button";
|
||
acceptBtn.textContent = "Annehmen";
|
||
acceptBtn.className = "btn btn-success btn-sm";
|
||
acceptBtn.addEventListener("click", () => respondToInvitation(inv.id, "accept", card));
|
||
|
||
const declineBtn = document.createElement("button");
|
||
declineBtn.type = "button";
|
||
declineBtn.textContent = "Ablehnen";
|
||
declineBtn.className = "btn btn-outline-secondary btn-sm";
|
||
declineBtn.addEventListener("click", () => respondToInvitation(inv.id, "decline", card));
|
||
|
||
btnGroup.append(acceptBtn, declineBtn, buildDeleteBtn(inv.id, card));
|
||
card.appendChild(btnGroup);
|
||
container.appendChild(card);
|
||
}
|
||
|
||
/** Erstellt den Löschen-Button – ruft DELETE /api/invitation/:id auf. */
|
||
function buildDeleteBtn(id, card) {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.textContent = "Löschen";
|
||
btn.className = "btn btn-outline-danger btn-sm";
|
||
btn.addEventListener("click", () => deleteInvitation(id, card));
|
||
return btn;
|
||
}
|
||
|
||
/**
|
||
* Aktualisiert den Einladungsstatus via PUT /api/invitation/:id.
|
||
* HTTP PUT gemäss REST: Update einer bestehenden Ressource.
|
||
*/
|
||
async function respondToInvitation(id, action, card) {
|
||
const status = action === "accept" ? "accepted" : "declined";
|
||
|
||
try {
|
||
const res = await fetch(`http://localhost:3000/api/invitation/${id}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ status })
|
||
});
|
||
|
||
if (!res.ok) throw new Error(`Status ${res.status}`);
|
||
|
||
const btnGroup = card.querySelector(".d-flex");
|
||
if (btnGroup) btnGroup.remove();
|
||
|
||
const statusEl = document.createElement("p");
|
||
statusEl.className = action === "accept" ? "text-success" : "text-muted";
|
||
statusEl.textContent = action === "accept" ? "✓ Angenommen" : "Abgelehnt";
|
||
card.appendChild(statusEl);
|
||
card.appendChild(buildDeleteBtn(id, card));
|
||
|
||
} catch (err) {
|
||
const errEl = document.createElement("p");
|
||
errEl.className = "text-danger small mt-1";
|
||
errEl.textContent = "Fehler beim Antworten auf die Einladung.";
|
||
card.appendChild(errEl);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Löscht eine Einladung via DELETE /api/invitation/:id.
|
||
* HTTP DELETE gemäss REST: Entfernt die Ressource dauerhaft.
|
||
* 204 No Content ist der erwartete Erfolgsstatus.
|
||
*/
|
||
async function deleteInvitation(id, card) {
|
||
try {
|
||
const res = await fetch(`http://localhost:3000/api/invitation/${id}`, {
|
||
method: "DELETE"
|
||
});
|
||
|
||
// 204 No Content = Erfolg; res.ok deckt 200–299 ab
|
||
if (!res.ok) throw new Error(`Status ${res.status}`);
|
||
|
||
card.remove();
|
||
|
||
} catch (err) {
|
||
const errEl = document.createElement("p");
|
||
errEl.className = "text-danger small mt-1";
|
||
errEl.textContent = "Fehler beim Löschen der Einladung.";
|
||
card.appendChild(errEl);
|
||
}
|
||
}
|
||
|
||
// =========================
|
||
// UTILITY – Fehlermeldung (XSS-sicher via textContent)
|
||
// =========================
|
||
function showError(container, message) {
|
||
const p = document.createElement("p");
|
||
p.className = "text-danger";
|
||
p.textContent = message;
|
||
container.innerHTML = "";
|
||
container.appendChild(p);
|
||
}
|