Improved authorization, added link to event cards #9
28
css/main.css
28
css/main.css
@ -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)
|
||||
========================= */
|
||||
|
||||
@ -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
BIN
js/.DS_Store
vendored
Normal file
Binary file not shown.
24
js/app.js
24
js/app.js
@ -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";
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user