"Event des Jahres": Aktivieren-Schalter, leer bis befuellt, Detail-Fenster

Nutzer-Wunsch 20.08.2026: "diese zwei kisten sollen leer sein solang wie
ich nichts rein setze, am besten sollen die sogar immer nur erscheinen
wenn ich was rein setze und sie aktiviere ... wen ich drauf druecke dass
dann ein kleines fenster aufgeht wo das bild bissl groesser ist mit einem
groesseren text und beschreibung, und mit einem link ... die kacheln
sollen auch bissl spezieller sein."

- Kein hartkodierter Standardinhalt mehr auf der Startseite -- die Kachel
  UND der ganze Abschnitt bleiben komplett unsichtbar, bis mindestens ein
  Event in der Verwaltung ausgefuellt UND ueber einen neuen "Aktiv"-Schalter
  freigeschaltet ist. Genau 1 aktives Event -> zentrierte Einzelkachel
  statt halbleerem Zwei-Spalten-Raster.
- Klick auf eine Kachel oeffnet jetzt ein Detail-Fenster (groesseres Bild,
  groesserer Titel/Text, optionaler direkter Link) statt sofort
  wegzunavigieren.
- Kacheln bekommen einen goldenen Trophaeen-Akzent + dezenten wandernden
  Lichtschimmer statt der neutralen Standardkarten-Optik.
- Bugfix unterwegs gefunden (Playwright-Screenshot): .jahres-event-card
  blieb trotz [hidden]-Attribut sichtbar (dieselbe Ursache wie der
  frühere .jahres-event-bild-Bug: display:block ueberschreibt die
  eingebaute [hidden]-Regel bei gleicher Spezifitaet) -- explizite
  [hidden]-Regel ergaenzt.
- Backend: server-internal/routes/events.js liefert oeffentlich NUR noch
  aktivierte Slots aus (getEventsPublic), neuer authentifizierter Endpunkt
  getEventsAdmin liefert der Verwaltung auch Entwuerfe zum Vorausfuellen.
  10 automatisierte Tests gegen eine Fake-DB bestanden.

Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn
manuell auf dogiintern ausrollen, sonst bleibt die Startseite beim alten
Verhalten (immer beide Slots zeigen, kein Aktiv-Schalter in der Verwaltung).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-20 22:43:28 +02:00
co-authored by Claude Opus 5
parent 208b5ba4ed
commit b3e191d3ca
39 changed files with 545 additions and 318 deletions
+24 -3
View File
@@ -40,9 +40,24 @@ function ladeSlot(slot) {
}
}
/** Öffentlich, keine Session: liefert beide Slots (oder null, wenn noch nie
gespeichert -- index.html behält dann seinen eingebauten Standardinhalt). */
/** Öffentlich, keine Session: liefert nur AKTIVIERTE Slots (Nutzer-Wunsch
20.08.2026: "die kisten sollen leer sein solang wie ich nichts rein
setze, am besten sollen die sogar immer nur erscheinen wenn ich was
rein setze und sie aktiviere") -- ein Slot mit gespeichertem Inhalt,
aber ohne gesetztes "aktiv", bleibt für Besucher:innen unsichtbar
(Entwurfsmodus), obwohl er in der Verwaltung längst vorausgefüllt ist. */
export async function getEventsPublic(req, res) {
const gibFreiWennAktiv = (slot) => {
const eintrag = ladeSlot(slot);
return eintrag && eintrag.aktiv ? eintrag : null;
};
return json(res, { ok: true, events: [gibFreiWennAktiv(1), gibFreiWennAktiv(2)] });
}
/** Für die Verwaltung: liefert BEIDE Slots roh (auch inaktive/Entwürfe),
damit das Formular beim Öffnen alles vorausfüllen kann. */
export async function getEventsAdmin(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
return json(res, { ok: true, events: [ladeSlot(1), ladeSlot(2)] });
}
@@ -66,6 +81,7 @@ export async function saveEvent(req, res, session) {
const textDe = String(body.text || "").trim().slice(0, 400);
const link = String(body.link || "").trim().slice(0, 300) || null;
const bild = String(body.bild || "").trim().slice(0, 300) || null;
const aktiv = !!body.aktiv;
if (!titelDe || !textDe) {
return json(res, { ok: false, error: "Titel und Text sind Pflicht." }, 400);
@@ -74,11 +90,16 @@ export async function saveEvent(req, res, session) {
if (link && !/^https?:\/\//i.test(link)) {
return json(res, { ok: false, error: "Link muss mit http:// oder https:// beginnen." }, 400);
}
// Aktivieren nur mit vollständigem Pflichtinhalt möglich -- verhindert ein
// leeres/kaputtes, aber "aktives" Feld auf der Startseite.
if (aktiv && (!titelDe || !textDe)) {
return json(res, { ok: false, error: "Zum Aktivieren müssen Titel und Text ausgefüllt sein." }, 400);
}
const [titel, text] = await Promise.all([uebersetzeAlleSprachen(titelDe), uebersetzeAlleSprachen(textDe)]);
const eintrag = {
datum, titel, text, link, bild,
datum, titel, text, link, bild, aktiv,
aktualisiertVon: session.actor,
aktualisiertAm: new Date().toISOString(),
};