Arbeit der parallelen Session in git gesichert (war nur auf dem Server)

Diese Änderungen liefen bereits live auf dem Server, lagen dort aber
ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy
(stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen.
Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird.

Enthalten (nicht von mir gebaut, nur gesichert):
- Supporter: eigener fester Zugangscode statt Einmalcode-Login
  (Migration 0009, lib/crypto.js scrypt-Hash, routes/supporter.js,
  abonnieren.html, supporter.html, i18n-abonnieren/-supporter)
- Stimmen: Profilbilder (Migration 0010, routes/testimonials.js)
- Event-Bild-Upload: voller Pfad statt relativem (routes/events.js)
- Neue/überarbeitete Hintergrundbilder für viele Seiten
- Teilen-Funktion (streamplan.js, i18n-index/-streamplan, main.css)

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-21 17:35:14 +02:00
co-authored by Claude Opus 5
parent 4b294f6c6b
commit 5d0ea11570
65 changed files with 992 additions and 139 deletions
+163 -16
View File
@@ -58,6 +58,80 @@
.poll-bar-track { height: 12px; border-radius: 999px; background: rgba(255,255,255,.06); overflow: hidden; border: 1px solid var(--border); }
.poll-bar-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent-2), var(--accent)); transition: width .4s ease; }
/* =====================================================================
Abschnitts-Kopf + Abmelde-Knopf (21.08.2026, Nutzer-Wunsch: "ich will
da einen richtig geilen abmelde button").
Farbwahl bewusst NICHT Gold: Gold ist auf der ganzen Seite die Farbe
für "das ist deine Belohnung / das willst du" (Prämien, Preis, Abo).
Ausloggen ist das Gegenteil — ein Weggehen. Deshalb ein gedecktes
Rosé/Rot aus derselben Edelstein-Familie wie die Verwaltungsseite:
eindeutig unterscheidbar, aber nicht das grelle Warnrot einer
Fehlermeldung (Projektregel "augenschonend"). Der Glanzstreifen läuft
nur beim Überfahren durch, nicht dauerhaft — ein permanent blinkender
Knopf in der Seitenmitte würde beim Lesen stören.
===================================================================== */
.supp-abschnitt-kopf {
display: flex; align-items: center; justify-content: space-between;
gap: 1rem 1.4rem; flex-wrap: wrap;
/* Nutzer-Hinweis 21.08.2026: "der abstand soll perfekter sein, teilweise
berühren die sich wenn man mit der maus über die kacheln geht."
Ursache war das margin:0 am h2 unten — dadurch lag der Kopf direkt auf
der Karte. Der reine Kasten-Abstand reicht hier aber nicht als Maß:
der Knopf wirft beim Überfahren einen Schatten von rund 32px nach
unten, und die Kacheln heben sich ihrerseits 4px an und leuchten
26px weit nach oben — beides zusammen berührte sich. Nachgemessen:
mit diesem Wert bleiben auch im ungünstigsten Fall (beide Hover-
Zustände gleichzeitig) deutlich sichtbare Luft dazwischen. */
margin: 0 0 1.9rem;
}
/* margin:0 nötig, damit die Überschrift in der Flex-Zeile sauber auf einer
Linie mit dem Knopf sitzt — der Abstand nach unten kommt stattdessen vom
Container oben. */
.supp-abschnitt-kopf h2 { margin: 0; }
.supp-logout {
position: relative; isolation: isolate; overflow: hidden;
display: inline-flex; align-items: center; gap: .55rem;
padding: .72rem 1.35rem; border-radius: 999px;
font-weight: 800; font-size: .92rem; letter-spacing: .01em;
cursor: pointer; white-space: nowrap;
color: #ffd9df;
border: 1.5px solid rgba(255,138,164,.45);
background: linear-gradient(140deg, rgba(74,26,38,.95), rgba(38,20,26,.95));
box-shadow: 0 8px 22px -14px rgba(255,110,140,.8), inset 0 1px 0 rgba(255,255,255,.06);
transition: transform .18s ease, box-shadow .22s ease, border-color .22s ease, color .22s ease;
}
/* Durchlaufender Glanz — startet außerhalb und wandert beim Hover einmal
quer über den Knopf. */
.supp-logout-glanz {
position: absolute; inset: 0; z-index: 1; pointer-events: none;
background: linear-gradient(100deg, transparent 34%, rgba(255,255,255,.42) 50%, transparent 66%);
background-size: 260% 100%;
background-position: 140% 0;
transition: background-position .75s ease;
}
.supp-logout > *:not(.supp-logout-glanz) { position: relative; z-index: 2; }
.supp-logout-icon { font-size: 1.02rem; line-height: 1; }
.supp-logout:hover {
transform: translateY(-2px);
color: #fff;
border-color: rgba(255,158,180,.85);
box-shadow: 0 14px 30px -12px rgba(255,110,140,.75), 0 0 22px rgba(255,110,140,.28), inset 0 1px 0 rgba(255,255,255,.1);
}
.supp-logout:hover .supp-logout-glanz { background-position: -60% 0; }
.supp-logout:active { transform: translateY(0) scale(.985); }
.supp-logout:focus-visible { outline: 2px solid rgba(255,158,180,.9); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
.supp-logout, .supp-logout-glanz { transition: none; }
.supp-logout:hover { transform: none; }
}
@media (max-width: 520px) {
/* Auf schmalen Handys volle Breite statt gequetscht neben der Überschrift. */
.supp-abschnitt-kopf { align-items: stretch; }
.supp-logout { width: 100%; justify-content: center; }
}
/* Gemeinsamer Karten-Rahmen mit Gold-Schimmerlinie oben — dieselbe
Machart wie .abo-preis-box auf abonnieren.html, damit der Bereich
erkennbar zusammengehört. */
@@ -322,12 +396,12 @@
}
</style>
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-abonnieren.jpg');--page-bg-mobile:url('/assets/img/bg-abonnieren-mobile.jpg');">
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-supporter.jpg');--page-bg-mobile:url('/assets/img/bg-supporter-mobile.jpg');">
<div id="site-header"></div>
<main>
<section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-abonnieren.jpg" alt="Dogfather Supporter-Abo" loading="eager" /></div>
<div class="hero-banner"><img src="/assets/img/bg-supporter.jpg" alt="Dogfather Supporter-Abo" loading="eager" /></div>
<div class="container">
<span class="eyebrow" data-i18n="su_eyebrow">👑 Supporter-Bereich</span>
<h1 id="supp-begruessung" data-i18n="su_lade">Dein Supporter-Bereich</h1>
@@ -351,7 +425,20 @@
<p class="abo-fehler" id="poll-fehler"></p>
</div>
<h2 data-i18n="su_fenster1_h2">Mein Abo</h2>
<!-- Nutzer-Wunsch 21.08.2026: "ich will da einen richtig geilen abmelde
button." Sitzt bewusst HIER oben neben der Überschrift statt wie
vorher ganz unten unter "Abo verwalten" — dort war er als schlichter
Umriss-Knopf kaum zu finden. Der alte wurde entfernt statt zusätzlich
stehen zu lassen: zwei Abmelde-Knöpfe auf einer Seite wären genau die
Dopplung, die schon bei "2 mal registrieren" gestört hat. -->
<div class="supp-abschnitt-kopf">
<h2 data-i18n="su_fenster1_h2">Mein Abo</h2>
<button type="button" class="supp-logout" id="btn-logout">
<span class="supp-logout-glanz" aria-hidden="true"></span>
<span class="supp-logout-icon" aria-hidden="true">⏻</span>
<span class="supp-logout-text" data-i18n="su_btn_logout">Ausloggen</span>
</button>
</div>
<div class="card supp-card">
<div class="supp-stats">
<div class="supp-stat"><span class="supp-stat-icon" aria-hidden="true">🪪</span><div><div class="label" data-i18n="su_stat_status">Status</div><div class="value" id="stat-status">–</div></div></div>
@@ -360,9 +447,20 @@
<div class="supp-stat supp-stat-clickable" id="stat-card-monate" tabindex="0" role="button" aria-haspopup="dialog"><span class="supp-stat-icon" aria-hidden="true">🗓️</span><div><div class="label" data-i18n="su_stat_monate">Bezahlte Monate gesamt</div><div class="value" id="stat-monate">–</div><div class="supp-stat-hinweis" data-i18n="su_stat_details_hinweis">Details ansehen</div></div></div>
<div class="supp-stat"><span class="supp-stat-icon" aria-hidden="true">🔄</span><div><div class="label" data-i18n="su_stat_zyklus">Aktueller Zyklus</div><div class="value" id="stat-zyklus">–</div></div></div>
</div>
<!-- Kein aktives Abo. Der frühere Knopf "Jetzt abonnieren" führte auf
abonnieren.html — die schickt eingeloggte Leute seit 21.08.2026
aber direkt wieder hierher zurück (siehe pruefeStatus() dort), das
wäre also eine Schleife gewesen. Solange die Bezahlung ohnehin
gesperrt ist, steht hier deshalb derselbe "Coming soon…"-Hinweis
wie auf der Anmeldeseite — ehrlicher und ohne Sackgasse. Sobald
PayPal live ist, wird hieraus wieder ein echter Knopf. -->
<div id="supp-keine-abo" style="display:none;margin-top:1.2rem;">
<p class="small" data-i18n="su_kein_abo_text">Du hast noch kein aktives Abo.</p>
<a class="btn btn-primary" href="abonnieren.html" data-i18n="su_kein_abo_btn">Jetzt abonnieren</a>
<div class="btn-gold-locked" style="max-width:320px;margin-top:.6rem;cursor:default;">
<span class="btn-gold-locked-lock" aria-hidden="true">🔒</span>
<span class="btn-gold-locked-text" data-i18n="su_kein_abo_soon">Coming soon…</span>
</div>
<p class="small muted" style="margin-top:.7rem;" data-i18n="su_kein_abo_hinweis">Alles hier ist schon für dich vorbereitet — sobald das Abo startet, kannst du es mit einem Klick aktivieren.</p>
</div>
</div>
@@ -388,9 +486,10 @@
<button type="button" class="btn btn-outline" id="btn-save-profile" data-i18n="su_btn_profil_speichern">Speichern</button>
<p class="abo-erfolg" id="profil-erfolg" data-i18n="su_profil_gespeichert">Gespeichert.</p>
<!-- "Ausloggen" ist von hier nach oben neben die Überschrift "Mein Abo"
gewandert (21.08.2026) — siehe dort. -->
<div class="btn-row" style="margin-top:1.6rem;">
<button type="button" class="btn btn-outline" id="btn-cancel" data-i18n="su_btn_kuendigen">Abo kündigen</button>
<button type="button" class="btn btn-outline" id="btn-logout" data-i18n="su_btn_logout">Ausloggen</button>
</div>
<p class="abo-fehler" id="verwalten-fehler"></p>
</div>
@@ -550,13 +649,29 @@
document.getElementById("supp-begruessung").textContent = `Willkommen, ${data.profile.displayName}!`;
const sub = data.subscription;
/* Läuft überhaupt ein Abo? Entscheidet, ob der nächste Meilenstein als
"läuft schon" (✨ + Countdown) oder als gesperrt (🔒) dargestellt wird.
Dieselbe Statusliste wie beim Nav-Knopf in main.js — "cancelled" zählt
bewusst noch dazu, weil der Zugang bis zum Ende des bezahlten Zeitraums
bestehen bleibt und in dieser Zeit auch noch Monate dazukommen können. */
const aboLaeuft = ["active", "pending", "suspended", "cancelled"].includes(sub.status);
document.getElementById("stat-status").innerHTML =
`<span class="supp-badge ${sub.status}">${STATUS_LABELS[sub.status] || sub.status}</span>`;
document.getElementById("stat-preis").textContent = sub.monthlyPrice;
/* Nutzer-Wunsch 21.08.2026: "da soll noch kein preis stehen bitte."
Der Preis ist überall sonst bewusst noch verborgen (auf abonnieren.html
steht an seiner Stelle "Coming soon…", siehe .abo-preis-zahl dort) —
über diese Kachel wäre er sonst durch die Hintertür doch schon
öffentlich gewesen. Wer bereits zahlt, MUSS seinen Preis dagegen sehen
können, deshalb nur ausblenden solange kein Abo läuft. */
document.getElementById("stat-preis").textContent = aboLaeuft ? sub.monthlyPrice : "Coming soon…";
document.getElementById("stat-naechste").textContent = fmtDatum(sub.nextPaymentAt);
document.getElementById("stat-monate").textContent = sub.totalPaidMonths;
document.getElementById("stat-zyklus").textContent = `Zyklus ${data.progress.cycleNumber}`;
document.getElementById("supp-keine-abo").style.display = sub.status === "none" ? "" : "none";
// Nutzer-Wunsch 21.08.2026: Kündigen-Knopf ergibt nur Sinn, wenn
// überhaupt ein (noch nicht schon gekündigtes) Abo existiert -- sonst
// sieht man einen Knopf für etwas, das es gar nicht gibt.
document.getElementById("btn-cancel").style.display = ["none", "cancelled", "expired"].includes(sub.status) ? "none" : "";
if (sub.status === "cancelled") {
const hinweis = document.getElementById("supp-kuendigungs-hinweis");
@@ -571,10 +686,17 @@
const zeitleisteEl = document.getElementById("supp-zeitleiste");
zeitleisteEl.innerHTML = t.milestones
.map((m) => {
const klasse = m.reached ? "erreicht" : m.isNext ? "naechste" : "";
const status = m.reached ? "Freigeschaltet" : m.isNext ? `Noch ${m.monthsRemaining} Monate` : "Gesperrt";
// Nutzer-Wunsch 21.08.2026: "die erste kiste soll blockiert sein wie
// die anderen, denn das ist ja verbunden mit dem abo." Ohne laufendes
// Abo läuft auch kein Countdown -- die Rechnung "noch 6 Monate" wäre
// schlicht falsch, weil der erste dieser Monate noch gar nicht
// angefangen hat. Deshalb: ohne Abo ist ALLES gesperrt, inklusive des
// ersten Meilensteins. Siehe aboLaeuft weiter oben.
const naechste = m.isNext && aboLaeuft;
const klasse = m.reached ? "erreicht" : naechste ? "naechste" : "";
const status = m.reached ? "Freigeschaltet" : naechste ? `Noch ${m.monthsRemaining} Monate` : "Gesperrt";
return `<div class="supp-meilenstein ${klasse}">
<div class="icon-medaille">${m.reached ? "🎁" : m.isNext ? "✨" : "🔒"}</div>
<div class="icon-medaille">${m.reached ? "🎁" : naechste ? "✨" : "🔒"}</div>
<div class="monat">Monat ${m.month}</div>
<div class="titel">${REDEEM_LABELS[m.type]}</div>
<div class="supp-premium-status">${status}</div>
@@ -775,6 +897,9 @@
function modalZahlungHtml(data) {
const sub = data.subscription;
// Gleiche Regel wie auf der Preis-Kachel: ohne laufendes Abo bleibt der
// Preis verborgen (siehe Kommentar bei stat-preis).
const aboLaeuftZahlung = ["active", "pending", "suspended", "cancelled"].includes(sub.status);
const payments = data.payments || [];
const gesamtBezahlt = payments
.filter((p) => p.payment_status === "completed")
@@ -801,7 +926,10 @@
</div>
<div class="supp-modal-grid">
<div><div class="supp-modal-label">Nächste Zahlung</div><div class="supp-modal-value">${fmtDatum(sub.nextPaymentAt)}</div></div>
<div><div class="supp-modal-label">Betrag</div><div class="supp-modal-value">${sub.monthlyPrice}</div></div>
<!-- Preis auch hier verbergen, solange kein Abo läuft (21.08.2026) —
sonst wäre er über den Klick auf "Nächste Zahlung" doch wieder
sichtbar, obwohl er auf der Kachel daneben schon verborgen ist. -->
<div><div class="supp-modal-label">Betrag</div><div class="supp-modal-value">${aboLaeuftZahlung ? sub.monthlyPrice : "Coming soon…"}</div></div>
<div><div class="supp-modal-label">Abo läuft seit</div><div class="supp-modal-value">${fmtDatum(sub.startedAt)}</div></div>
<div><div class="supp-modal-label">Zahlungsmethode</div><div class="supp-modal-value">PayPal</div></div>
${sub.cancelledAt ? `<div><div class="supp-modal-label">Gekündigt am</div><div class="supp-modal-value">${fmtDatum(sub.cancelledAt)}</div></div>` : ""}
@@ -816,15 +944,20 @@
function modalFortschrittHtml(data) {
const t = data.timeline;
const p = data.progress;
const naechster = t.milestones.find((m) => m.isNext);
// Gleiche Regel wie in der Zeitleiste oben (21.08.2026): ohne laufendes
// Abo ist auch der erste Meilenstein gesperrt, sonst stünden hier und
// dort widersprüchliche Angaben.
const aboLaeuftModal = ["active", "pending", "suspended", "cancelled"].includes(data.subscription.status);
const naechster = aboLaeuftModal ? t.milestones.find((m) => m.isNext) : null;
const prozent = Math.round((t.currentCycleMonth / 30) * 100);
const meilensteineHtml = t.milestones
.map((m) => {
const klasse = m.reached ? "erreicht" : m.isNext ? "naechste" : "";
const status = m.reached ? "Freigeschaltet" : m.isNext ? `noch ${m.monthsRemaining} Monate` : "gesperrt";
const naechste = m.isNext && aboLaeuftModal;
const klasse = m.reached ? "erreicht" : naechste ? "naechste" : "";
const status = m.reached ? "Freigeschaltet" : naechste ? `noch ${m.monthsRemaining} Monate` : "gesperrt";
return `<div class="supp-modal-meilenstein ${klasse}">
<span class="icon-medaille-mini" aria-hidden="true">${m.reached ? "🎁" : m.isNext ? "✨" : "🔒"}</span>
<span class="icon-medaille-mini" aria-hidden="true">${m.reached ? "🎁" : naechste ? "✨" : "🔒"}</span>
<div>
<div class="titel">${REDEEM_LABELS[m.type]}</div>
<div class="small" style="opacity:.75;">Monat ${m.month} · ${status}</div>
@@ -847,7 +980,12 @@
: ""
}
${
naechster
!aboLaeuftModal
? /* Ohne Abo darf hier NICHT "Zyklus komplett" stehen (das wäre der
Fall, wenn wirklich alle Meilensteine erreicht wären) — es hat
ja noch gar nichts angefangen. */
`<p class="small" style="margin-top:.9rem;">Alle Meilensteine sind noch gesperrt — sie starten, sobald dein Abo läuft.</p>`
: naechster
? `<p class="small" style="margin-top:.9rem;">Nächster Meilenstein: <strong>${REDEEM_LABELS[naechster.type]}</strong> in ${naechster.monthsRemaining} Monat${naechster.monthsRemaining === 1 ? "" : "en"}.</p>`
: `<p class="small" style="margin-top:.9rem;">Dieser Zyklus ist komplett — alle Prämien freigeschaltet! 🎉</p>`
}
@@ -870,7 +1008,16 @@
// vorherige Zeile im selben Klick trotzdem schon das Overlay
// sichtbar gemacht hätte. Zusätzlich try/catch um den Builder-Aufruf
// selbst, mit sichtbarer Konsolen-Meldung statt stillem Fehlschlag.
if (!letzteDaten || letzteDaten.subscription?.status === "none") return;
/* Nutzer-Wunsch 21.08.2026: "sie sollen ALLES SEHEN KÖNNEN." Die
frühere Bedingung blockierte das Detail-Fenster komplett, solange
kein Abo lief — die Kacheln laden aber sichtbar mit "Details
ansehen ›" zum Klicken ein, es passierte dann nur einfach nichts.
Beide Fenster kommen mit "kein Abo" sauber zurecht: die
Zahlungsübersicht zeigt "Noch keine Zahlungen erfasst.", die
Fortschritts-Ansicht alle Meilensteine gesperrt plus passenden
Hinweis (siehe modalFortschrittHtml). Deshalb bleibt hier nur noch
die Absicherung gegen fehlende Daten. */
if (!letzteDaten) return;
try {
oeffneModal(builder(letzteDaten));
} catch (err) {