Improved authorization, added link to event cards #9

Merged
thoenykaroli merged 1 commits from feature/authorirization_improvement into main 2026-06-01 13:17:34 +02:00
8 changed files with 130 additions and 15 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -310,7 +310,10 @@ main {
SECTION (EVENTS / SAVED / INVITATIONS)
========================= */
.events-section {
padding: 2.5rem 0 1rem;
/* Keep content away from the phone edges.
The section also has Bootstrap's .container class, but this rule appears later
and would otherwise overwrite the container's left/right padding with 0. */
padding: 2.5rem 1rem 1rem;
}
.events-section h2 {
@ -336,6 +339,7 @@ main {
flex-direction: column;
gap: 1rem;
max-width: 640px;
width: 100%;
}
/* =========================
@ -408,6 +412,28 @@ main {
margin: 0;
}
/* =========================
MOBILE SPACING FIX
Prevent cards/text from touching the viewport edges
========================= */
@media (max-width: 575.98px) {
.events-section {
padding-left: 1rem;
padding-right: 1rem;
}
.event-card,
.invitation-card {
padding: 1rem;
}
.invitation-card .d-flex,
.event-card .d-flex {
flex-wrap: wrap;
}
}
/* =========================
TABLET (768px)
========================= */

View File

@ -27,7 +27,7 @@
<ul class="nav" role="list">
<li class="nav-item">
<a class="nav-link" href="#" id="nav-events" aria-label="Zur Event-Suche">Events</a>
<a class="nav-link d-none" href="#" id="nav-events" aria-label="Zur Event-Suche">Events</a>
</li>
<li class="nav-item">
<a class="nav-link d-none" href="#" id="nav-my-events" aria-label="Meine gespeicherten Events">My Events</a>

BIN
js/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -16,6 +16,7 @@ import { createEventCard } from "./ui/eventCard.js";
// =========================
let currentUser = null;
let currentPassword = null;
let lastRenderedEvents = [];
window.currentUser = null;
window.currentPassword = null;
@ -120,8 +121,11 @@ modalLoginBtn.addEventListener("click", async () => {
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;
@ -203,10 +207,12 @@ logoutBtn.addEventListener("click", () => {
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();
});
// =========================
@ -235,6 +241,7 @@ async function handleSearch() {
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.");
@ -287,6 +294,13 @@ navInv.addEventListener("click", (e) => {
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");
@ -393,6 +407,16 @@ function renderInvitation(inv, container) {
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";

View File

@ -8,7 +8,7 @@ import { fetchEvents } from "../api/ticketmaster.js";
/**
* Lädt und transformiert Events für eine gegebene Stadt.
* @param {string} city - Stadtname
* @returns {Promise<Array<{id, name, date, time, venue, category}>>}
* @returns {Promise<Array<{id, name, date, time, venue, category, url}>>}
*/
export async function getEvents(city) {
const events = await fetchEvents(city);
@ -20,6 +20,7 @@ export async function getEvents(city) {
date: event.dates?.start?.localDate || null,
time: event.dates?.start?.localTime || null,
venue: event._embedded?.venues?.[0]?.name || "Unbekannter Ort",
url: event.url || null,
// Kategorie aus dem ersten Klassifizierungs-Segment extrahieren (z.B. "Music", "Sports")
category: event.classifications?.[0]?.segment?.name
? event.classifications[0].segment.name.toLowerCase()

View File

@ -41,6 +41,21 @@ export function createEventCard(event) {
article.append(title, date, venue);
if (event.url) {
const link = document.createElement("a");
link.href = event.url;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.className = "btn btn-outline-secondary btn-sm mt-2";
link.textContent = "Event ansehen";
article.appendChild(link);
}
// Gäste sehen nur Event-Informationen. Speichern und Einladen sind Login-Funktionen.
if (!window.currentUser) {
return article;
}
// --- BUTTON-GRUPPE ---
const buttonContainer = document.createElement("div");
buttonContainer.className = "d-flex gap-2 mt-2 flex-wrap";
@ -154,7 +169,8 @@ function createInviteForm(event, card) {
body: JSON.stringify({
toUser,
eventId: event.id,
eventName: event.name
eventName: event.name,
eventUrl: event.url || null
})
});

View File

@ -111,9 +111,25 @@ app.get("/api/user", (req, res) => {
let invitations = [];
let idCounter = 1;
/**
* Normalisiert Benutzernamen für Vergleiche.
* Dadurch werden z.B. "Anna" und " anna " als derselbe Benutzer behandelt.
*/
function normalizeUsername(username) {
return String(username || "").trim().toLowerCase();
}
/**
* Erstellt einen stabilen Event-Schlüssel für den Duplikat-Check.
* Ticketmaster liefert normalerweise eine id. Falls diese fehlt, verwenden wir URL oder Namen.
*/
function getEventKey({ eventId, eventUrl, eventName }) {
return String(eventId || eventUrl || eventName || "").trim().toLowerCase();
}
/**
* POST /api/invitation Einladung senden.
* Erwartet: Header X-Username (Sender), Body { toUser, eventId, eventName }
* Erwartet: Header X-Username (Sender), Body { toUser, eventId, eventName, eventUrl }
* Antwortet mit 201 Created und dem erstellten Einladungsobjekt.
*/
app.post("/api/invitation", (req, res) => {
@ -121,29 +137,60 @@ app.post("/api/invitation", (req, res) => {
return res.status(400).json({ message: "Request-Body fehlt" });
}
const fromUser = req.header("X-Username");
const { toUser, eventId, eventName } = req.body;
const fromUserRaw = req.header("X-Username");
const { toUser: toUserRaw, eventId, eventName, eventUrl } = req.body;
const fromUser = String(fromUserRaw || "").trim();
const toUser = String(toUserRaw || "").trim();
const fromUserKey = normalizeUsername(fromUser);
const toUserKey = normalizeUsername(toUser);
const eventKey = getEventKey({ eventId, eventUrl, eventName });
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)) {
if (!eventKey) {
return res.status(400).json({ message: "Event fehlt. Einladung kann nicht gespeichert werden." });
}
// Prüfen ob der Empfänger registriert ist.
// Case-insensitive, damit "Anna" und "anna" nicht als verschiedene Nutzer behandelt werden.
const recipient = users.find(u => normalizeUsername(u.username) === toUserKey);
if (!recipient) {
return res.status(404).json({ message: `Nutzer "${toUser}" ist nicht registriert.` });
}
// Nicht sich selbst einladen
if (fromUser === toUser) {
if (fromUserKey === toUserKey) {
return res.status(400).json({ message: "Du kannst dich nicht selbst einladen." });
}
// Doppelte Einladung verhindern:
// Derselbe Sender darf denselben Empfänger nicht mehrfach zum selben Event einladen.
// Wichtig: Der Vergleich läuft über normalisierte Keys, nicht über direkte Strings.
const duplicateInvitation = invitations.find(inv =>
inv.fromUserKey === fromUserKey &&
inv.toUserKey === toUserKey &&
inv.eventKey === eventKey
);
if (duplicateInvitation) {
return res.status(409).json({
message: `Du hast "${recipient.username}" bereits zu diesem Event eingeladen.`
});
}
const invitation = {
id: idCounter++,
fromUser,
toUser,
eventId,
fromUserKey,
toUser: recipient.username,
toUserKey,
eventId: eventId || null,
eventKey,
eventName,
eventUrl: eventUrl || null,
status: "pending"
};
@ -163,7 +210,8 @@ app.get("/api/invitation", (req, res) => {
return res.status(400).json({ message: "Benutzername fehlt" });
}
const userInvitations = invitations.filter(inv => inv.toUser === user);
const userKey = normalizeUsername(user);
const userInvitations = invitations.filter(inv => inv.toUserKey === userKey);
res.json(userInvitations);
});