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)
|
SECTION (EVENTS / SAVED / INVITATIONS)
|
||||||
========================= */
|
========================= */
|
||||||
.events-section {
|
.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 {
|
.events-section h2 {
|
||||||
@ -336,6 +339,7 @@ main {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -408,6 +412,28 @@ main {
|
|||||||
margin: 0;
|
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)
|
TABLET (≥768px)
|
||||||
========================= */
|
========================= */
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<ul class="nav" role="list">
|
<ul class="nav" role="list">
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link d-none" href="#" id="nav-my-events" aria-label="Meine gespeicherten Events">My Events</a>
|
<a class="nav-link d-none" href="#" id="nav-my-events" aria-label="Meine gespeicherten Events">My Events</a>
|
||||||
@ -212,4 +212,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
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 currentUser = null;
|
||||||
let currentPassword = null;
|
let currentPassword = null;
|
||||||
|
let lastRenderedEvents = [];
|
||||||
window.currentUser = null;
|
window.currentUser = null;
|
||||||
window.currentPassword = null;
|
window.currentPassword = null;
|
||||||
|
|
||||||
@ -120,8 +121,11 @@ modalLoginBtn.addEventListener("click", async () => {
|
|||||||
userArea.classList.remove("d-none");
|
userArea.classList.remove("d-none");
|
||||||
userNameEl.textContent = `👤 ${username}`;
|
userNameEl.textContent = `👤 ${username}`;
|
||||||
|
|
||||||
|
navEvents.classList.remove("d-none");
|
||||||
navSaved.classList.remove("d-none");
|
navSaved.classList.remove("d-none");
|
||||||
navInv.classList.remove("d-none");
|
navInv.classList.remove("d-none");
|
||||||
|
|
||||||
|
refreshVisibleEventCards();
|
||||||
} else {
|
} else {
|
||||||
loginError.textContent = "Login fehlgeschlagen. Bitte Zugangsdaten prüfen.";
|
loginError.textContent = "Login fehlgeschlagen. Bitte Zugangsdaten prüfen.";
|
||||||
loginError.hidden = false;
|
loginError.hidden = false;
|
||||||
@ -203,10 +207,12 @@ logoutBtn.addEventListener("click", () => {
|
|||||||
|
|
||||||
authArea.classList.remove("d-none");
|
authArea.classList.remove("d-none");
|
||||||
userArea.classList.add("d-none");
|
userArea.classList.add("d-none");
|
||||||
|
navEvents.classList.add("d-none");
|
||||||
navSaved.classList.add("d-none");
|
navSaved.classList.add("d-none");
|
||||||
navInv.classList.add("d-none");
|
navInv.classList.add("d-none");
|
||||||
|
|
||||||
showSection("events");
|
showSection("events");
|
||||||
|
refreshVisibleEventCards();
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
@ -235,6 +241,7 @@ async function handleSearch() {
|
|||||||
try {
|
try {
|
||||||
const events = await getEvents(city);
|
const events = await getEvents(city);
|
||||||
const filtered = applyFilters(events, dateFrom, dateTo, category);
|
const filtered = applyFilters(events, dateFrom, dateTo, category);
|
||||||
|
lastRenderedEvents = filtered;
|
||||||
renderEventList(filtered, eventListEl);
|
renderEventList(filtered, eventListEl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(eventListEl, "Fehler beim Laden der Events. Bitte erneut versuchen.");
|
showError(eventListEl, "Fehler beim Laden der Events. Bitte erneut versuchen.");
|
||||||
@ -287,6 +294,13 @@ navInv.addEventListener("click", (e) => {
|
|||||||
loadInvitations();
|
loadInvitations();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function refreshVisibleEventCards() {
|
||||||
|
if (!eventsSection.classList.contains("d-none") && lastRenderedEvents.length > 0) {
|
||||||
|
renderEventList(lastRenderedEvents, eventListEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showSection(section) {
|
function showSection(section) {
|
||||||
searchSection.classList.add("d-none");
|
searchSection.classList.add("d-none");
|
||||||
eventsSection.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.");
|
text.append(strong, " hat dich zu ", em, " eingeladen.");
|
||||||
card.appendChild(text);
|
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") {
|
if (inv.status !== "pending") {
|
||||||
const status = document.createElement("p");
|
const status = document.createElement("p");
|
||||||
status.className = inv.status === "accepted" ? "text-success" : "text-muted";
|
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.
|
* Lädt und transformiert Events für eine gegebene Stadt.
|
||||||
* @param {string} city - Stadtname
|
* @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) {
|
export async function getEvents(city) {
|
||||||
const events = await fetchEvents(city);
|
const events = await fetchEvents(city);
|
||||||
@ -20,9 +20,10 @@ export async function getEvents(city) {
|
|||||||
date: event.dates?.start?.localDate || null,
|
date: event.dates?.start?.localDate || null,
|
||||||
time: event.dates?.start?.localTime || null,
|
time: event.dates?.start?.localTime || null,
|
||||||
venue: event._embedded?.venues?.[0]?.name || "Unbekannter Ort",
|
venue: event._embedded?.venues?.[0]?.name || "Unbekannter Ort",
|
||||||
|
url: event.url || null,
|
||||||
// Kategorie aus dem ersten Klassifizierungs-Segment extrahieren (z.B. "Music", "Sports")
|
// Kategorie aus dem ersten Klassifizierungs-Segment extrahieren (z.B. "Music", "Sports")
|
||||||
category: event.classifications?.[0]?.segment?.name
|
category: event.classifications?.[0]?.segment?.name
|
||||||
? event.classifications[0].segment.name.toLowerCase()
|
? event.classifications[0].segment.name.toLowerCase()
|
||||||
: null
|
: null
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -41,6 +41,21 @@ export function createEventCard(event) {
|
|||||||
|
|
||||||
article.append(title, date, venue);
|
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 ---
|
// --- BUTTON-GRUPPE ---
|
||||||
const buttonContainer = document.createElement("div");
|
const buttonContainer = document.createElement("div");
|
||||||
buttonContainer.className = "d-flex gap-2 mt-2 flex-wrap";
|
buttonContainer.className = "d-flex gap-2 mt-2 flex-wrap";
|
||||||
@ -154,7 +169,8 @@ function createInviteForm(event, card) {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
toUser,
|
toUser,
|
||||||
eventId: event.id,
|
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 invitations = [];
|
||||||
let idCounter = 1;
|
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.
|
* 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.
|
* Antwortet mit 201 Created und dem erstellten Einladungsobjekt.
|
||||||
*/
|
*/
|
||||||
app.post("/api/invitation", (req, res) => {
|
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" });
|
return res.status(400).json({ message: "Request-Body fehlt" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromUser = req.header("X-Username");
|
const fromUserRaw = req.header("X-Username");
|
||||||
const { toUser, eventId, eventName } = req.body;
|
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) {
|
if (!fromUser || !toUser) {
|
||||||
return res.status(400).json({ message: "Sender und Empfänger sind erforderlich" });
|
return res.status(400).json({ message: "Sender und Empfänger sind erforderlich" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen ob der Empfänger registriert ist
|
if (!eventKey) {
|
||||||
if (!users.find(u => u.username === toUser)) {
|
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.` });
|
return res.status(404).json({ message: `Nutzer "${toUser}" ist nicht registriert.` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nicht sich selbst einladen
|
// Nicht sich selbst einladen
|
||||||
if (fromUser === toUser) {
|
if (fromUserKey === toUserKey) {
|
||||||
return res.status(400).json({ message: "Du kannst dich nicht selbst einladen." });
|
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 = {
|
const invitation = {
|
||||||
id: idCounter++,
|
id: idCounter++,
|
||||||
fromUser,
|
fromUser,
|
||||||
toUser,
|
fromUserKey,
|
||||||
eventId,
|
toUser: recipient.username,
|
||||||
|
toUserKey,
|
||||||
|
eventId: eventId || null,
|
||||||
|
eventKey,
|
||||||
eventName,
|
eventName,
|
||||||
|
eventUrl: eventUrl || null,
|
||||||
status: "pending"
|
status: "pending"
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -163,7 +210,8 @@ app.get("/api/invitation", (req, res) => {
|
|||||||
return res.status(400).json({ message: "Benutzername fehlt" });
|
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);
|
res.json(userInvitations);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user