From fdffc08417fcd36f404316693a7e992e69e22a8a Mon Sep 17 00:00:00 2001 From: Nandrx Date: Sun, 31 May 2026 17:48:39 +0200 Subject: [PATCH 1/3] Improve code quality, security, and accessibility - Fix login check: use HTTP 200 instead of 201 for GET /api/user - Add encodeURIComponent for city in Ticketmaster URL - Re-throw errors in ticketmaster.js so app.js can show user feedback - Add JSDoc comments to all service and utility modules - Add German labels and text throughout (de-CH locale) --- js/api/ticketmaster.js | 23 ++++++++++++++++++----- js/auth.js | 33 ++++++++++++++++++++++++++------- js/services/eventService.js | 24 ++++++++++++++++++------ js/ui/filters.js | 23 ++++++++++++++++------- 4 files changed, 78 insertions(+), 25 deletions(-) diff --git a/js/api/ticketmaster.js b/js/api/ticketmaster.js index e841f4d..252096c 100644 --- a/js/api/ticketmaster.js +++ b/js/api/ticketmaster.js @@ -1,19 +1,32 @@ +/** + * ticketmaster.js – Schnittstelle zum Backend-Proxy für die Ticketmaster Discovery API. + * Der direkte API-Aufruf läuft über den eigenen Backend-Server, um den API-Key + * nicht im Frontend zu exponieren (OWASP: Sensitive Data Exposure vermeiden). + */ + +/** + * Lädt Events für eine Stadt über den Backend-Proxy. + * @param {string} city - Stadtname für die Suche + * @returns {Promise} Array von rohen Ticketmaster-Event-Objekten + */ export async function fetchEvents(city) { - const url = `http://localhost:3000/api/events?city=${city}`; + const url = `http://localhost:3000/api/events?city=${encodeURIComponent(city)}`; try { const response = await fetch(url); if (!response.ok) { - throw new Error("API request failed"); + throw new Error(`Backend antwortete mit Status ${response.status}`); } const data = await response.json(); + // Ticketmaster gibt Events unter _embedded.events zurück; leeres Array als Fallback return data._embedded?.events || []; } catch (error) { - console.error("Error fetching events:", error); - return []; + console.error("Fehler beim Laden der Events:", error); + // Fehler nach oben weitergeben, damit app.js ihn dem Nutzer anzeigen kann + throw error; } -} \ No newline at end of file +} diff --git a/js/auth.js b/js/auth.js index c7582ec..94354d2 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,20 +1,38 @@ +/** + * auth.js – Kommunikation mit dem Backend für Registrierung und Login. + * Nutzt die Fetch-API mit einfachem Header-basiertem Authentifizierungsschema. + * + * Sicherheitshinweis: Passwörter werden unverschlüsselt als HTTP-Header übertragen. + * Dies ist bewusst vereinfacht (kein TLS/HTTPS in der lokalen Entwicklungsumgebung). + * In einer Produktionsumgebung müssten JWT-Tokens oder Sessions eingesetzt werden. + */ + const BASE_URL = "http://localhost:3000/api"; +/** + * Registriert einen neuen Benutzer beim Backend. + * Das Backend vergibt ein Standard-Passwort ("1234"). + * @param {string} username - Gewünschter Benutzername + * @returns {Promise} Objekt mit name und password + */ export async function register(username) { - const res = await fetch(`${BASE_URL}/user`, { method: "POST", - headers: { - "Content-Type": "application/json" - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username }) }); return res.json(); } +/** + * Meldet einen Benutzer an. + * Credentials werden als HTTP-Header übermittelt (X-Username, X-Password). + * @param {string} username + * @param {string} password + * @returns {Promise} true bei Erfolg (HTTP 200), false bei falschen Credentials + */ export async function login(username, password) { - const res = await fetch(`${BASE_URL}/user`, { method: "GET", headers: { @@ -23,5 +41,6 @@ export async function login(username, password) { } }); - return res.status === 201; -} \ No newline at end of file + // HTTP 200 = Anmeldung erfolgreich, 401 = ungültige Credentials + return res.status === 200; +} diff --git a/js/services/eventService.js b/js/services/eventService.js index eb320a6..6f8aa06 100644 --- a/js/services/eventService.js +++ b/js/services/eventService.js @@ -1,16 +1,28 @@ +/** + * eventService.js – Transformiert rohe Ticketmaster-API-Daten in ein vereinfachtes Event-Objekt. + * Kapselt die Datenstruktur der externen API, sodass der Rest der App davon unabhängig bleibt. + */ + import { fetchEvents } from "../api/ticketmaster.js"; +/** + * Lädt und transformiert Events für eine gegebene Stadt. + * @param {string} city - Stadtname + * @returns {Promise>} + */ export async function getEvents(city) { const events = await fetchEvents(city); + // Rohe API-Daten auf ein einheitliches, einfaches Format abbilden return events.map(event => ({ - id: event.id, - name: event.name, - date: event.dates?.start?.localDate || null, - time: event.dates?.start?.localTime || null, - venue: event._embedded?.venues?.[0]?.name || "Unknown venue", + id: event.id, + name: event.name, + date: event.dates?.start?.localDate || null, + time: event.dates?.start?.localTime || null, + venue: event._embedded?.venues?.[0]?.name || "Unbekannter Ort", + // Kategorie aus dem ersten Klassifizierungs-Segment extrahieren (z.B. "Music", "Sports") category: event.classifications?.[0]?.segment?.name ? event.classifications[0].segment.name.toLowerCase() : null })); -} \ No newline at end of file +} diff --git a/js/ui/filters.js b/js/ui/filters.js index b2ac6bb..e566e04 100644 --- a/js/ui/filters.js +++ b/js/ui/filters.js @@ -1,8 +1,17 @@ +/** + * filters.js – Liest die aktuellen Filterwerte aus den Formularfeldern. + * Gibt ein einheitliches Filter-Objekt zurück, das von app.js ausgewertet wird. + */ + +/** + * Liest Stadtname, Datumsbereich und Kategorie aus den Eingabefeldern. + * @returns {{ city: string, dateFrom: string, dateTo: string, category: string }} + */ export function getFilters() { - const city = document.querySelector("#city-input").value.trim(); - const dateFrom = document.querySelector("#date-from").value; - const dateTo = document.querySelector("#date-to").value; - const category = document.querySelector("#category-input").value; - - return { city, dateFrom, dateTo, category }; - } \ No newline at end of file + const city = document.querySelector("#city-input").value.trim(); + const dateFrom = document.querySelector("#date-from").value; + const dateTo = document.querySelector("#date-to").value; + const category = document.querySelector("#category-input").value; + + return { city, dateFrom, dateTo, category }; +} -- 2.30.2 From ae3692697bbfec655c32f5a8df566a6157ecbb3c Mon Sep 17 00:00:00 2001 From: Nandrx Date: Sun, 31 May 2026 17:52:25 +0200 Subject: [PATCH 2/3] Add REST-compliant PUT/DELETE endpoints and invitation validation - Replace POST /invitation/:id/accept|decline with PUT /invitation/:id - Add DELETE /invitation/:id (full CRUD: GET, POST, PUT, DELETE) - Validate invitation recipient exists before creating invitation (404) - Block self-invitation (400) - Propagate server error messages to inline card feedback - Add Loeschen button to invitation cards (demonstrates DELETE in UI) - Improve empty state with icon and descriptive text --- js/ui/eventCard.js | 201 +++++++++++++++++++++++++---------- js/ui/eventList.js | 34 +++++- server/server.js | 254 ++++++++++++++++++++++++++------------------- 3 files changed, 324 insertions(+), 165 deletions(-) diff --git a/js/ui/eventCard.js b/js/ui/eventCard.js index c9f84fd..1f66809 100644 --- a/js/ui/eventCard.js +++ b/js/ui/eventCard.js @@ -1,114 +1,205 @@ +/** + * eventCard.js – Erstellt interaktive Event-Karten als DOM-Elemente. + * Jede Karte zeigt Titel, Datum, Veranstaltungsort sowie Speichern- und Einladen-Buttons. + */ + +/** + * Erstellt eine vollständige Event-Karte als
-Element. + * @param {Object} event - Event-Objekt mit id, name, date, time, venue, category + * @returns {HTMLElement} Fertig aufgebautes Karten-Element + */ export function createEventCard(event) { const article = document.createElement("article"); article.className = "event-card"; + // --- TITEL --- const title = document.createElement("h3"); title.className = "event-card__title"; title.textContent = event.name; + // --- DATUM & UHRZEIT formatieren (Schweizer Format: dd.mm.yyyy) --- const formattedDate = event.date ? new Date(event.date).toLocaleDateString("de-CH", { day: "2-digit", month: "2-digit", year: "numeric" }) - : "Date not available"; + : "Datum unbekannt"; - const formattedTime = event.time - ? event.time.slice(0, 5) - : ""; - - const dateTime = formattedTime - ? `${formattedDate}, ${formattedTime}` - : formattedDate; + const formattedTime = event.time ? event.time.slice(0, 5) : ""; + const dateTime = formattedTime ? `${formattedDate}, ${formattedTime}` : formattedDate; const date = document.createElement("p"); date.className = "event-card__date"; date.textContent = dateTime; + // --- VERANSTALTUNGSORT --- const venue = document.createElement("p"); venue.className = "event-card__venue"; - venue.textContent = event.venue; + venue.textContent = event.venue || "Ort unbekannt"; article.append(title, date, venue); - // ========================= - // BUTTON CONTAINER - // ========================= + // --- BUTTON-GRUPPE --- const buttonContainer = document.createElement("div"); - buttonContainer.className = "d-flex gap-2 mt-2"; + buttonContainer.className = "d-flex gap-2 mt-2 flex-wrap"; // ========================= - // SAVE BUTTON + // SPEICHERN-BUTTON + // Speichert das Event im localStorage, verhindert Duplikate // ========================= const saveBtn = document.createElement("button"); - saveBtn.textContent = "Save"; + saveBtn.type = "button"; saveBtn.className = "btn btn-outline-primary btn-sm"; + // Prüfen ob bereits gespeichert → Button-Text entsprechend setzen + const alreadySaved = isAlreadySaved(event.id); + saveBtn.textContent = alreadySaved ? "Gespeichert ✓" : "Speichern"; + if (alreadySaved) saveBtn.disabled = true; + saveBtn.addEventListener("click", () => { - - if (!window.currentUser) { - alert("Please login to save events"); - return; - } - const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]"); - // prevent duplicates + // Duplikat-Schutz if (saved.find(e => e.id === event.id)) { - alert("Event already saved"); + saveBtn.textContent = "Gespeichert ✓"; + saveBtn.disabled = true; return; } saved.push(event); localStorage.setItem("savedEvents", JSON.stringify(saved)); - alert("Event saved!"); + // Visuelles Feedback statt alert() + saveBtn.textContent = "Gespeichert ✓"; + saveBtn.disabled = true; + saveBtn.classList.replace("btn-outline-primary", "btn-success"); }); - saveBtn.disabled = !window.currentUser; - // ========================= - // INVITE BUTTON + // EINLADEN-BUTTON + // Zeigt ein Inline-Formular an (kein browser-prompt()) // ========================= const inviteBtn = document.createElement("button"); - inviteBtn.textContent = "Invite"; + inviteBtn.type = "button"; + inviteBtn.textContent = "Einladen"; inviteBtn.className = "btn btn-primary btn-sm"; + inviteBtn.disabled = false; - inviteBtn.addEventListener("click", async () => { - + inviteBtn.addEventListener("click", () => { if (!window.currentUser) { - alert("Please login to invite users"); + showCardMessage(article, "Bitte einloggen um Einladungen zu senden.", "warning"); return; } - - const toUser = prompt("Enter username to invite:"); - if (!toUser) return; - - await fetch("http://localhost:3000/api/invitation", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Username": window.currentUser - }, - body: JSON.stringify({ - toUser, - eventId: event.id, - eventName: event.name - }) - }); - - alert("Invitation sent!"); + // Formular ein-/ausblenden (Toggle) + const existing = article.querySelector(".invite-form"); + if (existing) { + existing.remove(); + } else { + article.appendChild(createInviteForm(event, article)); + } }); - inviteBtn.disabled = !window.currentUser; - - // ========================= - // APPEND BUTTONS - // ========================= buttonContainer.append(saveBtn, inviteBtn); article.appendChild(buttonContainer); return article; -} \ No newline at end of file +} + +// ========================= +// HILFSFUNKTION – Einladungsformular +// Erstellt ein kleines Inline-Formular mit Benutzername-Eingabe +// ========================= +function createInviteForm(event, card) { + const form = document.createElement("div"); + form.className = "invite-form mt-2 d-flex gap-2 align-items-center flex-wrap"; + + const input = document.createElement("input"); + input.type = "text"; + input.className = "form-control form-control-sm"; + input.placeholder = "Benutzername eingeben"; + input.setAttribute("aria-label", "Benutzername für Einladung"); + + const sendBtn = document.createElement("button"); + sendBtn.type = "button"; + sendBtn.textContent = "Senden"; + sendBtn.className = "btn btn-primary btn-sm"; + + const cancelBtn = document.createElement("button"); + cancelBtn.type = "button"; + cancelBtn.textContent = "Abbrechen"; + cancelBtn.className = "btn btn-outline-secondary btn-sm"; + + cancelBtn.addEventListener("click", () => form.remove()); + + sendBtn.addEventListener("click", async () => { + const toUser = input.value.trim(); + if (!toUser) { + input.classList.add("is-invalid"); + return; + } + input.classList.remove("is-invalid"); + + sendBtn.disabled = true; + sendBtn.textContent = "Sende..."; + + try { + const res = await fetch("http://localhost:3000/api/invitation", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Username": window.currentUser + }, + body: JSON.stringify({ + toUser, + eventId: event.id, + eventName: event.name + }) + }); + + if (!res.ok) { + // Fehlermeldung vom Server auslesen (z.B. "Nutzer nicht registriert") + const errData = await res.json().catch(() => ({})); + throw new Error(errData.message || `Fehler ${res.status}`); + } + + // Formular entfernen und Erfolgsmeldung anzeigen + form.remove(); + showCardMessage(card, `Einladung an "${toUser}" gesendet.`, "success"); + + } catch (err) { + sendBtn.disabled = false; + sendBtn.textContent = "Senden"; + showCardMessage(card, err.message || "Fehler beim Senden der Einladung.", "danger"); + console.error("Einladungs-Fehler:", err); + } + }); + + form.append(input, sendBtn, cancelBtn); + return form; +} + +// ========================= +// HILFSFUNKTION – Status-Nachricht in Karte anzeigen +// Zeigt eine kurze Meldung innerhalb der Karte an und blendet sie nach 3s aus +// ========================= +function showCardMessage(card, message, type) { + // Alte Nachrichten entfernen + card.querySelectorAll(".card-message").forEach(el => el.remove()); + + const msg = document.createElement("p"); + msg.className = `card-message text-${type} mt-1 mb-0`; + msg.textContent = message; + card.appendChild(msg); + + setTimeout(() => msg.remove(), 3000); +} + +// ========================= +// HILFSFUNKTION – Prüft ob Event bereits gespeichert ist +// ========================= +function isAlreadySaved(eventId) { + const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]"); + return saved.some(e => e.id === eventId); +} diff --git a/js/ui/eventList.js b/js/ui/eventList.js index 7310723..b6584c5 100644 --- a/js/ui/eventList.js +++ b/js/ui/eventList.js @@ -1,11 +1,38 @@ +/** + * eventList.js – Rendert eine Liste von Event-Karten in einen Container. + * Delegiert die Erstellung einzelner Karten an createEventCard(). + */ + import { createEventCard } from "./eventCard.js"; +/** + * Leert den Container und fügt für jedes Event eine Karte ein. + * Zeigt eine Meldung an, wenn keine Events gefunden wurden. + * @param {Array} events - Array von Event-Objekten + * @param {HTMLElement} container - Ziel-DOM-Element + */ export function renderEventList(events, container) { - container.innerHTML = ""; if (events.length === 0) { - container.innerHTML = "No events found."; + const state = document.createElement("div"); + state.className = "empty-state"; + + const icon = document.createElement("div"); + icon.className = "empty-state__icon"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = "🎭"; + + const title = document.createElement("p"); + title.className = "empty-state__text"; + title.textContent = "Keine Events gefunden"; + + const sub = document.createElement("p"); + sub.className = "empty-state__sub"; + sub.textContent = "Versuche eine andere Stadt oder erweitere den Datumsbereich."; + + state.append(icon, title, sub); + container.appendChild(state); return; } @@ -13,5 +40,4 @@ export function renderEventList(events, container) { const card = createEventCard(event); container.appendChild(card); }); - -} \ No newline at end of file +} diff --git a/server/server.js b/server/server.js index 27c5743..4dd6fef 100644 --- a/server/server.js +++ b/server/server.js @@ -1,167 +1,209 @@ +/** + * server.js – Express-Backend für Encore. + * + * Funktionen: + * - Proxy für die Ticketmaster Discovery API (API-Key bleibt serverseitig) + * - Einfache Benutzerverwaltung (In-Memory, kein Persistenz-Layer) + * - Einladungssystem zwischen registrierten Benutzern + * + * Sicherheitshinweise (OWASP): + * - A02 Cryptographic Failures: Passwörter werden nicht gehasht (bewusste Vereinfachung) + * - A07 Identification and Authentication Failures: Keine Tokens/Sessions, nur Header-Auth + * - Der API-Key liegt serverseitig in .env und wird nie an den Client übermittelt + */ + import express from "express"; -import cors from "cors"; -import dotenv from "dotenv"; +import cors from "cors"; +import dotenv from "dotenv"; + dotenv.config(); const app = express(); -// Middleware +// Middleware: CORS erlaubt Anfragen vom Frontend (localhost), JSON-Body parsen app.use(cors()); app.use(express.json()); // ========================= -// ROOT +// ROOT – Health-Check // ========================= app.get("/", (req, res) => { res.send("Encore API is running"); }); // ========================= -// TICKETMASTER EVENTS (PROXY) +// TICKETMASTER-PROXY +// Versteckt den API-Key vor dem Client (A02: Sensitive Data Exposure vermeiden) +// GET /api/events?city={city} // ========================= const TM_API_KEY = process.env.TM_API_KEY; app.get("/api/events", async (req, res) => { - const city = req.query.city; if (!city) { - return res.status(400).json({ message: "City is required" }); + return res.status(400).json({ message: "Stadt ist erforderlich" }); } try { - const url = `https://app.ticketmaster.com/discovery/v2/events.json?apikey=${TM_API_KEY}&city=${city}`; - + const url = `https://app.ticketmaster.com/discovery/v2/events.json?apikey=${TM_API_KEY}&city=${encodeURIComponent(city)}`; const response = await fetch(url); - const data = await response.json(); + const data = await response.json(); res.json(data); - } catch (error) { - console.error("Event fetch error:", error); - res.status(500).json({ message: "Error fetching events" }); + console.error("Ticketmaster-Fehler:", error); + res.status(500).json({ message: "Fehler beim Laden der Events" }); } }); // ========================= -// INVITATIONS SYSTEM +// BENUTZERVERWALTUNG +// In-Memory-Speicher (Daten gehen bei Serverneustart verloren – bewusste Vereinfachung) // ========================= -let invitations = []; -let idCounter = 1; - -// SEND INVITATION -app.post("/api/invitation", (req, res) => { - - if (!req.body) { - return res.status(400).json({ message: "Missing request body" }); - } - - const fromUser = req.header("X-Username"); - const { toUser, eventId, eventName } = req.body; - - if (!fromUser || !toUser) { - return res.status(400).json({ message: "Missing users" }); - } - - const invitation = { - id: idCounter++, - fromUser, - toUser, - eventId, - eventName, - status: "pending" - }; - - invitations.push(invitation); - - res.json(invitation); - }); - -// GET INVITATIONS FOR USER -app.get("/api/invitation", (req, res) => { - - const user = req.header("X-Username"); - - if (!user) { - return res.status(400).json({ message: "Missing username" }); - } - - const userInvitations = invitations.filter( - inv => inv.toUser === user - ); - - res.json(userInvitations); -}); - -// ACCEPT INVITATION -app.post("/api/invitation/:id/accept", (req, res) => { - - const inv = invitations.find(i => i.id == req.params.id); - - if (!inv) { - return res.status(404).json({ message: "Invitation not found" }); - } - - inv.status = "accepted"; - - res.json(inv); -}); - -// DECLINE INVITATION -app.post("/api/invitation/:id/decline", (req, res) => { - - const inv = invitations.find(i => i.id == req.params.id); - - if (!inv) { - return res.status(404).json({ message: "Invitation not found" }); - } - - inv.status = "declined"; - - res.json(inv); -}); - let users = []; -// CREATE USER +/** + * POST /api/user – Neuen Benutzer registrieren. + * Vergibt ein Standard-Passwort "1234" (vereinfacht, kein Hashing). + * Antwortet mit 201 Created bei Erfolg, 400 bei fehlendem/doppeltem Benutzernamen. + */ app.post("/api/user", (req, res) => { - const { username } = req.body; if (!username) { - return res.status(400).json({ message: "Username required" }); + return res.status(400).json({ message: "Benutzername erforderlich" }); } if (users.find(u => u.username === username)) { - return res.status(400).json({ message: "User exists" }); + return res.status(400).json({ message: "Benutzer existiert bereits" }); } const password = "1234"; + users.push({ username, password }); - const user = { username, password }; - users.push(user); - - res.json({ name: username, password }); + res.status(201).json({ name: username, password }); }); -// LOGIN +/** + * GET /api/user – Benutzer anmelden. + * Credentials kommen als X-Username und X-Password Header. + * Antwortet mit 200 OK bei Erfolg, 401 Unauthorized bei falschen Credentials. + * + * Hinweis: GET für eine Login-Operation ist unkonventionell (normalerweise POST). + * Hier bewusst so gewählt, da kein Session/Token-Mechanismus implementiert ist. + */ app.get("/api/user", (req, res) => { - const username = req.header("X-Username"); const password = req.header("X-Password"); - const user = users.find( - u => u.username === username && u.password === password - ); + const user = users.find(u => u.username === username && u.password === password); if (!user) return res.sendStatus(401); - res.sendStatus(201); + res.sendStatus(200); }); // ========================= -// START SERVER +// EINLADUNGSSYSTEM +// Verwaltet Event-Einladungen zwischen Benutzern +// ========================= +let invitations = []; +let idCounter = 1; + +/** + * POST /api/invitation – Einladung senden. + * Erwartet: Header X-Username (Sender), Body { toUser, eventId, eventName } + * Antwortet mit 201 Created und dem erstellten Einladungsobjekt. + */ +app.post("/api/invitation", (req, res) => { + if (!req.body) { + return res.status(400).json({ message: "Request-Body fehlt" }); + } + + const fromUser = req.header("X-Username"); + const { toUser, eventId, eventName } = req.body; + + if (!fromUser || !toUser) { + return res.status(400).json({ message: "Sender und Empfänger sind erforderlich" }); + } + + // Prüfen ob der Empfänger registriert ist + if (!users.find(u => u.username === toUser)) { + return res.status(404).json({ message: `Nutzer "${toUser}" ist nicht registriert.` }); + } + + // Nicht sich selbst einladen + if (fromUser === toUser) { + return res.status(400).json({ message: "Du kannst dich nicht selbst einladen." }); + } + + const invitation = { + id: idCounter++, + fromUser, + toUser, + eventId, + eventName, + status: "pending" + }; + + invitations.push(invitation); + res.status(201).json(invitation); +}); + +/** + * GET /api/invitation – Alle Einladungen für einen Benutzer abrufen. + * Erwartet: Header X-Username (Empfänger) + * Antwortet mit 200 und Array der Einladungen. + */ +app.get("/api/invitation", (req, res) => { + const user = req.header("X-Username"); + + if (!user) { + return res.status(400).json({ message: "Benutzername fehlt" }); + } + + const userInvitations = invitations.filter(inv => inv.toUser === user); + res.json(userInvitations); +}); + +/** + * PUT /api/invitation/:id – Einladungsstatus aktualisieren (annehmen oder ablehnen). + * Erwartet: Body { status: "accepted" | "declined" } + * HTTP-Verb PUT gemäss REST: Update einer bestehenden Ressource. + * Antwortet mit 200 OK und dem aktualisierten Einladungsobjekt. + */ +app.put("/api/invitation/:id", (req, res) => { + const inv = invitations.find(i => i.id == req.params.id); + + if (!inv) return res.status(404).json({ message: "Einladung nicht gefunden" }); + + const { status } = req.body; + if (!["accepted", "declined"].includes(status)) { + return res.status(400).json({ message: "Ungültiger Status. Erlaubt: accepted, declined" }); + } + + inv.status = status; + res.json(inv); +}); + +/** + * DELETE /api/invitation/:id – Einladung löschen. + * HTTP-Verb DELETE gemäss REST: Entfernt die Ressource dauerhaft. + * Antwortet mit 204 No Content bei Erfolg. + */ +app.delete("/api/invitation/:id", (req, res) => { + const index = invitations.findIndex(i => i.id == req.params.id); + + if (index === -1) return res.status(404).json({ message: "Einladung nicht gefunden" }); + + invitations.splice(index, 1); + res.sendStatus(204); // 204 No Content – Ressource erfolgreich gelöscht +}); + +// ========================= +// SERVER STARTEN // ========================= app.listen(3000, () => { - console.log("Server running on http://localhost:3000"); -}); \ No newline at end of file + console.log("Server läuft auf http://localhost:3000"); +}); -- 2.30.2 From 53f9691376b829a41df25bde4d04e266c8c23330 Mon Sep 17 00:00:00 2001 From: Nandrx Date: Sun, 31 May 2026 17:52:40 +0200 Subject: [PATCH 3/3] Redesign frontend with hero section, auth modal, and animations - Replace header auth inputs with Bootstrap modal (Login/Register tabs) - Add hero section with dark gradient, decorative grid lines, teal glows - Floating search card with strong shadow inside hero - Add card entrance animation (fadeSlideUp) with staggered nth-child delays - Add skeleton loader shown while events are fetching - Add empty state components with emoji icon and description - Remove all alert() calls, replace with inline DOM error messages - Add CSS custom properties for consistent design tokens - Add Inter font, modern typography scale - Document input validation under OWASP A03 in README --- README.md | 161 ++++++++++++--- css/components.css | 400 +++++++++++++++++++++++++++++++----- css/main.css | 437 +++++++++++++++++++++++++++++++++------ index.html | 241 ++++++++++++++-------- js/app.js | 498 +++++++++++++++++++++++++++++++++------------ 5 files changed, 1378 insertions(+), 359 deletions(-) diff --git a/README.md b/README.md index c9e60f9..f619cff 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,150 @@ -# Encore -Frontend Project for cds-208 at FHGR -Collaborators: Pascal Schieman, Nicola Augsburger, Karolina Thöny-Tyganova -## Project Description -**Find your next event in seconds.** Encore is a modern event discovery frontend -that brings Ticketmaster events directly to users through a clean, intelligent -interface. Instead of sifting through cluttered listings, users can instantly browse -concerts, sports, theater, and entertainment by location, date, and category—then -dive into event details to make informed decisions fast. +# Encore – Event Discovery -The product centers on three core goals: -- **Discoverability**: make events easy to find through location and category filters. -- **Decision support**: display essential details (date, venue, city, lineup) at a glance. -- **Usability**: deliver a responsive, intuitive interface that works seamlessly on all devices. -- **Community interaction**: provide event-specific discussion areas with interactive commenting and @-mentions so users can tag and notify each other directly. -- **Social coordination**: enable direct, personalized event invitations between registered users, supported by notifications and dashboard-based invitation management. +Frontend-Projekt für das Modul cds-208 an der FHGR (Frühlingssemester 2026). -Built on Ticketmaster's API, Encore provides real-time event data while maintaining -a lightweight, scalable architecture—ready for future features like personalized -recommendations, saved events, social sharing, and collaborative event planning. +--- + +## Projektbeschreibung + +**Encore** ist eine webbasierte Event-Discovery-Plattform. Nutzer können Konzerte, Sportveranstaltungen und Kulturanlässe in jeder Stadt direkt über die Ticketmaster-API suchen und filtern – ohne sich durch überladene Buchungsportale kämpfen zu müssen. + +Die drei Kernziele der Anwendung: + +- **Entdecken**: Events nach Stadt, Datum und Kategorie filtern +- **Organisieren**: Interessante Events lokal speichern und in der eigenen Event-Liste verwalten +- **Teilen**: Andere registrierte Nutzer direkt zu Events einladen und auf Einladungen reagieren + +--- + +## Besondere Features + +| Feature | Beschreibung | +|---|---| +| **Ticketmaster-Integration** | Live-Daten über einen eigenen Backend-Proxy (API-Key bleibt serverseitig) | +| **Dreifachfilter** | Gleichzeitig nach Stadt, Datumsbereich und Kategorie filtern | +| **My Events** | Gespeicherte Events in einer eigenen Ansicht mit Entfernen-Option | +| **Einladungssystem** | Nutzer können andere per Inline-Formular zu Events einladen und Einladungen annehmen/ablehnen | +| **Vollständig responsiv** | Optimiert für Mobile, Tablet und Desktop (Bootstrap-Grid + eigenes CSS) | +| **XSS-Schutz** | Alle nutzerkontrollierten Daten werden via `textContent` und DOM-Methoden gerendert (kein `innerHTML` mit externen Daten) | +| **Accessibility (WCAG A)** | Semantisches HTML5, ARIA-Labels, sichtbare Labels für alle Formularfelder, `aria-live`-Regionen | + +--- + +## Installation & lokale Ausführung + +### 1. Repository klonen -## Setup and run -### Clone the repository ```bash git clone https://gitea.fhgr.ch/augsbunicola/AISE_FS26-FrontendEntwicklungProjekt.git cd AISE_FS26-FrontendEntwicklungProjekt ``` -### Install backend dependencies + +### 2. Abhängigkeiten installieren ```bash npm install ``` -### Configure environment variables (at the moment API key is open to test) -### Start the backend server +### 3. Umgebungsvariable konfigurieren + +Erstelle eine `.env`-Datei im Projektstamm: + +```env +TM_API_KEY=dein_ticketmaster_api_key +``` + +Den API-Key erhältst du kostenlos unter [developer.ticketmaster.com](https://developer.ticketmaster.com). +*(Zum Testen ist im Repository vorübergehend ein Key hinterlegt.)* + +### 4. Backend-Server starten ```bash node server/server.js ``` -you should see: -```txt -Server running on http://localhost:3000 -``` -### Open index.html in your browser \ No newline at end of file +Ausgabe: `Server läuft auf http://localhost:3000` + +### 5. Frontend öffnen + +Öffne `index.html` direkt im Browser (kein separater Dev-Server nötig). + +> **Tipp:** Verwende die Live Server Extension in VS Code für automatisches Reload. + +--- + +## Verwendete Technologien, Libraries & Frameworks + +| Kategorie | Technologie | Begründung | +|---|---|---| +| **Markup** | HTML5 (semantisch) | Strukturierung mit `
`, `
`, `