523 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 200299 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);
}