Alle ausstehenden Aenderungen (Bilder, Texte) fuer den Server-Umzug uebernommen

This commit is contained in:
2026-08-05 18:05:53 +02:00
parent 9af0698056
commit e331965bf1
87 changed files with 8296 additions and 309 deletions
+867
View File
@@ -0,0 +1,867 @@
<!DOCTYPE html>
<html lang="de" data-theme="dogfather">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mein Supporter-Bereich — DOGFATHER UNIVERSE</title>
<meta name="description" content="Dein persönlicher Supporter-Bereich: Abo-Status, Fortschritt und Treueprämien." />
<link rel="icon" type="image/png" href="assets/img/favicon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0b0d10" />
<meta name="robots" content="noindex" />
<link rel="stylesheet" href="assets/css/main.css" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" />
<style>
/* =====================================================================
Supporter-Bereich — komplett neu gestaltet (Nutzer-Feedback
03.08.2026: "die Kisten müssen mega geil sein, wirklich so ultra
ultra geil, so speziell, lass es krachen"). Statt schlichtem Text auf
Karten jetzt: eigenes Gold/Champagner-Farbthema NUR innerhalb dieses
Bereichs (Supporter = etwas Besonderes, bewusst anders als das normale
Babyblau der restlichen Seite), Icon-Medaillons pro Kennzahl,
verbundene Fortschritts-Zeitleiste mit Leuchtpfad, Kollektionskarten
mit Ribbon-Nummer.
Der Trick für das Farbthema: main.css baut praktisch JEDEN Knopf/jede
Karte über die CSS-Variablen --accent/--accent-2 (Aurora-Glow,
Hover-Rahmen, .btn-primary-Füllung, ...). Statt jeden Knopf einzeln
neu zu stylen, wird hier einmalig auf #supp-root umdefiniert — alles
darunter (auch btn-outline/btn-primary/.card:hover) erbt automatisch
Gold statt Babyblau, ganz ohne die Basis-Komponenten anzufassen. */
#supp-root {
--accent: #ffe9a8;
--accent-2: #d4af37;
--accent-contrast: #1a1408;
}
.supp-hinweis { padding: .8rem 1rem; border-radius: 10px; background: rgba(212,175,55,.08); border: 1px solid rgba(212,175,55,.3); color: #ffe9a8; font-size: .9rem; margin-bottom: 1rem; }
.abo-fehler { color: #ff9a9a; font-size: .88rem; margin-top: .5rem; display: none; }
.abo-fehler.zeige { display: block; }
.abo-erfolg { color: #baf7d6; font-size: .88rem; margin-top: .5rem; display: none; }
.abo-erfolg.zeige { display: block; }
/* Gemeinsamer Karten-Rahmen mit Gold-Schimmerlinie oben — dieselbe
Machart wie .abo-preis-box auf abonnieren.html, damit der Bereich
erkennbar zusammengehört. */
.supp-card {
position: relative; overflow: hidden;
border: 1.5px solid rgba(212,175,55,.35);
background: radial-gradient(circle at 18% 0%, rgba(212,175,55,.1), transparent 55%),
linear-gradient(180deg, rgba(23,27,33,.92), rgba(14,16,20,.96));
}
.supp-card::before {
content: ""; position: absolute; top: 0; left: 6%; right: 6%; height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,214,102,.85), transparent);
animation: supp-goldglanz 4.5s ease-in-out infinite;
}
@keyframes supp-goldglanz { 0%,100% { opacity: .3; } 50% { opacity: 1; } }
/* ---------- Mein Abo: Kennzahlen als eigene Prunk-Kärtchen ----------
Komplett neu (Nutzer-Feedback 03.08.2026: "die Titel sollen auch in
der Kiste sein, und die Kisten sollen viel viel viel viel viel
spezieller aussehen"). Vorher: Icon links, Label+Wert rechts daneben
— bei zweizeiligen Labels ("Aktuelle Kollektion") wirkte das
unausgerichtet, als würde der Titel aus der Karte rausragen. Jetzt:
senkrechter Aufbau (Medaillon oben, Titel darunter, Wert darunter),
jede Karte garantiert in sich geschlossen, plus ein eigener
Gold-Rahmen mit wanderndem Schimmer-Rand (zweischichtiger
Hintergrund-Trick — siehe dokumentierte Lektion: NIE einen ganzen
Ring per transform:rotate() drehen, das verzerrt auf eckigen Karten;
hier läuft stattdessen nur die Hintergrundposition). */
.supp-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1.1rem; }
.supp-stat {
position: relative; isolation: isolate; overflow: hidden;
display: flex; flex-direction: column; align-items: flex-start; gap: .6rem;
border-radius: 16px; padding: 1.3rem 1.2rem 1.4rem;
border: 1.5px solid transparent;
background:
linear-gradient(rgba(20,23,29,.94), rgba(12,14,18,.96)) padding-box,
linear-gradient(120deg, rgba(212,175,55,.15), rgba(255,233,168,.55), rgba(212,175,55,.15), rgba(139,92,246,.25)) border-box;
background-size: 100% 100%, 280% 100%;
animation: supp-stat-shimmer 7s linear infinite;
box-shadow: 0 14px 30px -18px rgba(0,0,0,.7);
transition: transform .2s ease, box-shadow .2s ease;
}
@keyframes supp-stat-shimmer { to { background-position: 0 0, -280% 0; } }
.supp-stat::after {
content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
background: radial-gradient(circle at 18% 0%, rgba(212,175,55,.14), transparent 55%);
pointer-events: none;
}
.supp-stat:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px -16px rgba(0,0,0,.75), 0 0 26px rgba(212,175,55,.28);
}
.supp-stat .supp-stat-icon {
flex: none; width: 50px; height: 50px; border-radius: 50%; font-size: 1.4rem;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 30% 25%, rgba(255,233,168,.45), rgba(212,175,55,.1) 72%);
border: 1.5px solid rgba(212,175,55,.45);
box-shadow: 0 0 16px rgba(212,175,55,.25), inset 0 0 10px rgba(255,255,255,.06);
}
.supp-stat .label {
font-size: .72rem; font-weight: 700; color: #ffe9a8; margin-top: .2rem;
text-transform: uppercase; letter-spacing: .06em; opacity: .85;
}
.supp-stat .value { font-size: 1.2rem; font-weight: 800; line-height: 1.3; }
@media (prefers-reduced-motion: reduce) { .supp-stat { animation: none; } }
/* ---------- Klickbare Kennzahl-Kärtchen -> Detail-Modal (Nutzer-Wunsch
04.08.2026: "wenn man auf diese 2 Kisten drückt, sollen die viel mehr
Infos kriegen ... richtig geil"). Eigener Hinweis-Chip unten in der
Karte macht die Klickbarkeit sichtbar, statt sie erraten zu müssen. */
.supp-stat-clickable { cursor: pointer; }
.supp-stat-clickable:focus-visible { outline: 2px solid #ffe9a8; outline-offset: 3px; }
.supp-stat-clickable .supp-stat-hinweis {
margin-top: .15rem; font-size: .68rem; color: #ffe9a8; opacity: .65;
display: flex; align-items: center; gap: .25rem; font-weight: 600;
}
.supp-stat-clickable:hover .supp-stat-hinweis,
.supp-stat-clickable:focus-visible .supp-stat-hinweis { opacity: 1; }
.supp-stat-clickable .supp-stat-hinweis::after { content: ""; font-size: .95rem; transition: transform .18s ease; }
.supp-stat-clickable:hover .supp-stat-hinweis::after { transform: translateX(3px); }
/* ---------- Detail-Modal ---------- */
.supp-modal-overlay {
position: fixed; inset: 0; z-index: 999;
background: rgba(6,7,9,.72); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
display: flex; align-items: center; justify-content: center; padding: 4vh 1rem;
animation: supp-modal-fade .18s ease-out;
}
/* KRITISCHER BUGFIX 04.08.2026: ".supp-modal-overlay { display:flex }" hat
dieselbe Spezifität wie die Browser-Standardregel "[hidden]{display:none}"
— Autor-CSS gewinnt bei Gleichstand IMMER gegen die UA-Stylesheet-Regel,
das [hidden]-Attribut wurde dadurch komplett wirkungslos. Ergebnis: das
Modal war die GANZE ZEIT sichtbar (leer, weil #supp-modal-body erst bei
echtem Öffnen befüllt wird) — unabhängig von jedem Klick, auf jedem
Seitenaufruf von supporter.html, in jedem Browser (auch Inkognito).
Exakt derselbe Bug-Typ wie schon einmal bei .btn-tiktok/.abo-btn-discord
auf abonnieren.html — hier aber übersehen, weil meine eigenen Tests nur
das JS-"hidden"-Property geprüft haben (korrekt: true), nie den
TATSÄCHLICH gerenderten CSS-display-Wert. Fix: explizite Ausnahme. */
#supp-modal-overlay[hidden] { display: none; }
@keyframes supp-modal-fade { from { opacity: 0; } to { opacity: 1; } }
.supp-modal {
position: relative; width: min(560px, 100%); max-height: 90vh; overflow-y: auto;
border-radius: 20px; padding: 1.9rem 1.8rem 2rem;
border: 1.5px solid transparent;
background:
linear-gradient(rgba(16,18,23,.98), rgba(9,10,13,.99)) padding-box,
linear-gradient(120deg, rgba(212,175,55,.5), rgba(255,233,168,.9), rgba(139,92,246,.4), rgba(212,175,55,.5)) border-box;
background-size: 100% 100%, 280% 100%;
animation: supp-modal-in .28s cubic-bezier(.2,.9,.25,1), supp-stat-shimmer 8s linear infinite;
box-shadow: 0 30px 70px -20px rgba(0,0,0,.85), 0 0 60px -10px rgba(212,175,55,.25);
}
@keyframes supp-modal-in { from { opacity: 0; transform: translateY(14px) scale(.97); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .supp-modal { animation: none; } .supp-modal-overlay { animation: none; } }
.supp-modal-close {
position: absolute; top: 1rem; right: 1rem; width: 34px; height: 34px; border-radius: 50%;
display: flex; align-items: center; justify-content: center; font-size: 1rem; line-height: 1;
background: rgba(255,255,255,.05); border: 1.5px solid var(--border); color: var(--text);
cursor: pointer; transition: background .15s ease, transform .15s ease, border-color .15s ease;
}
.supp-modal-close:hover { background: rgba(212,175,55,.15); border-color: rgba(212,175,55,.5); transform: rotate(90deg); }
.supp-modal-head { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin-bottom: 1.3rem; padding-right: 2.2rem; }
.supp-modal-icon {
flex: none; width: 46px; height: 46px; border-radius: 50%; font-size: 1.3rem;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 30% 25%, rgba(255,233,168,.5), rgba(212,175,55,.12) 72%);
border: 1.5px solid rgba(212,175,55,.5); box-shadow: 0 0 18px rgba(212,175,55,.3);
}
.supp-modal-head h3 { margin: 0; font-size: 1.25rem; flex: 1 1 auto; min-width: 140px; }
.supp-modal-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: .9rem 1.1rem; margin-bottom: 1.4rem; }
.supp-modal-label { font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; color: #ffe9a8; opacity: .8; font-weight: 700; }
.supp-modal-value { font-size: 1.02rem; font-weight: 700; margin-top: .15rem; }
.supp-modal-subh { font-size: .82rem; text-transform: uppercase; letter-spacing: .06em; color: #ffe9a8; opacity: .85; margin: 1.5rem 0 .7rem; }
.supp-pay-liste { display: flex; flex-direction: column; gap: .45rem; max-height: 240px; overflow-y: auto; padding-right: .3rem; }
.supp-pay-row {
display: flex; align-items: center; justify-content: space-between; gap: .8rem;
padding: .6rem .8rem; border-radius: 10px; background: rgba(255,255,255,.03); border: 1px solid var(--border);
font-size: .87rem;
}
.supp-pay-datum { color: var(--text-muted); flex: 1 1 auto; }
.supp-pay-betrag { font-weight: 700; }
.supp-pay-status { font-size: .74rem; font-weight: 700; padding: .18rem .55rem; border-radius: 999px; white-space: nowrap; }
.supp-pay-status.status-completed { background: rgba(52,211,153,.15); color: #5eeaa7; }
.supp-pay-status.status-failed { background: rgba(217,74,92,.15); color: #ff9a9a; }
.supp-pay-status.status-refunded, .supp-pay-status.status-reversed { background: rgba(143,217,234,.15); color: #8fd9ea; }
.supp-modal-hero-zahl { font-size: 2.4rem; font-weight: 900; line-height: 1; margin-bottom: 1.1rem; }
.supp-modal-hero-zahl .small { display: block; font-size: .82rem; font-weight: 600; color: var(--text-muted); margin-top: .35rem; }
.supp-progress-bar { height: 12px; border-radius: 999px; background: rgba(255,255,255,.06); overflow: hidden; border: 1px solid var(--border); }
.supp-progress-fill {
height: 100%; border-radius: 999px;
background: linear-gradient(90deg, #d4af37, #ffe9a8, #d4af37);
background-size: 220% 100%; animation: abo-btn-shine 5s ease-in-out infinite;
box-shadow: 0 0 14px rgba(212,175,55,.5);
transition: width .5s ease;
}
.supp-modal-celebrate {
margin-top: 1rem; padding: .8rem 1rem; border-radius: 12px; font-size: .9rem; font-weight: 700;
background: rgba(212,175,55,.1); border: 1px solid rgba(212,175,55,.35); color: #ffe9a8;
}
.supp-modal-meilensteine { display: flex; flex-direction: column; gap: .55rem; }
.supp-modal-meilenstein {
display: flex; align-items: center; gap: .8rem; padding: .65rem .85rem; border-radius: 12px;
background: rgba(255,255,255,.02); border: 1px solid var(--border);
}
.supp-modal-meilenstein.erreicht { border-color: rgba(212,175,55,.5); background: rgba(212,175,55,.06); }
.supp-modal-meilenstein.naechste { border-color: rgba(143,217,234,.5); background: rgba(143,217,234,.06); }
.icon-medaille-mini {
flex: none; width: 36px; height: 36px; border-radius: 50%; font-size: 1.05rem;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 32% 26%, rgba(255,255,255,.12), transparent 65%);
border: 1.5px solid var(--border);
}
.supp-modal-meilenstein.erreicht .icon-medaille-mini { border-color: #ffe9a8; background: radial-gradient(circle at 32% 26%, rgba(255,233,168,.55), rgba(212,175,55,.12) 70%); }
.supp-modal-meilenstein.naechste .icon-medaille-mini { border-color: #8fd9ea; background: radial-gradient(circle at 32% 26%, rgba(143,217,234,.4), transparent 70%); }
.supp-modal-meilenstein .titel { font-weight: 700; font-size: .92rem; }
.supp-badge { display: inline-flex; align-items: center; gap: .35rem; padding: .32rem .75rem; border-radius: 999px; font-size: .78rem; font-weight: 700; }
.supp-badge.active { background: rgba(52,211,153,.15); color: #5eeaa7; box-shadow: 0 0 14px rgba(52,211,153,.25); }
.supp-badge.pending { background: rgba(212,175,55,.15); color: #ffe9a8; box-shadow: 0 0 14px rgba(212,175,55,.25); }
.supp-badge.cancelled { background: rgba(217,74,92,.15); color: #ff9a9a; }
.supp-badge.none, .supp-badge.suspended, .supp-badge.expired { background: rgba(255,255,255,.08); color: var(--text-muted); }
/* ---------- Fortschritt: verbundener Leuchtpfad mit Medaillons ---------- */
.supp-zeitleiste { position: relative; display: flex; flex-wrap: wrap; gap: 1.4rem 1rem; margin-top: 1.6rem; padding-top: 1rem; }
.supp-meilenstein {
position: relative; flex: 1 1 140px; text-align: center; padding: 1.2rem .7rem .9rem;
border-radius: 14px; border: 1.5px solid var(--border); background: rgba(255,255,255,.02);
transition: transform .18s ease, border-color .18s ease;
}
.supp-meilenstein.erreicht { border-color: rgba(212,175,55,.55); background: rgba(212,175,55,.07); }
.supp-meilenstein.naechste { border-color: rgba(143,217,234,.55); background: rgba(143,217,234,.07); animation: supp-naechste-puls 2.6s ease-in-out infinite; }
@keyframes supp-naechste-puls {
0%, 100% { box-shadow: 0 0 0 rgba(143,217,234,0); }
50% { box-shadow: 0 0 22px rgba(143,217,234,.35); }
}
.supp-meilenstein:hover { transform: translateY(-3px); }
.supp-meilenstein .icon-medaille {
width: 54px; height: 54px; margin: 0 auto .5rem; border-radius: 50%;
display: flex; align-items: center; justify-content: center; font-size: 1.6rem;
background: radial-gradient(circle at 32% 26%, rgba(255,255,255,.12), transparent 65%);
border: 1.5px solid var(--border);
}
.supp-meilenstein.erreicht .icon-medaille { border-color: #ffe9a8; background: radial-gradient(circle at 32% 26%, rgba(255,233,168,.55), rgba(212,175,55,.12) 70%); box-shadow: 0 0 18px rgba(212,175,55,.4); }
.supp-meilenstein.naechste .icon-medaille { border-color: #8fd9ea; background: radial-gradient(circle at 32% 26%, rgba(143,217,234,.4), transparent 70%); }
.supp-meilenstein .monat { font-size: .78rem; color: var(--text-muted); }
.supp-meilenstein .titel { font-weight: 800; font-size: .9rem; margin-top: .2rem; }
.supp-meilenstein .supp-premium-status { margin-top: .3rem; }
/* ---------- Sammlungsarchiv: Kollektionskarten mit Ribbon ---------- */
.supp-kollektion {
position: relative; overflow: hidden; border-radius: 14px; padding: 1.4rem 1.4rem 1.2rem; margin-bottom: 1.2rem;
border: 1.5px solid rgba(212,175,55,.3);
background: radial-gradient(circle at 90% 0%, rgba(212,175,55,.08), transparent 55%), rgba(255,255,255,.02);
}
.supp-kollektion h3 { display: flex; align-items: center; gap: .5rem; font-size: 1.1rem; }
.supp-kollektion h3::before { content: "🏆"; font-size: 1.15rem; }
.supp-premium-row { display: flex; justify-content: space-between; align-items: center; padding: .7rem 0; border-bottom: 1px dashed var(--border); gap: 1rem; flex-wrap: wrap; }
.supp-premium-row:last-child { border-bottom: none; }
.supp-premium-status {
display: inline-block; font-size: .76rem; font-weight: 700; padding: .2rem .55rem; border-radius: 999px;
color: var(--text-muted); background: rgba(255,255,255,.06);
}
.supp-premium-status.status-verfuegbar { background: rgba(212,175,55,.15); color: #ffe9a8; }
.supp-premium-status.status-versandbereit,
.supp-premium-status.status-versendet { background: rgba(143,217,234,.15); color: #8fd9ea; }
.supp-premium-status.status-zugestellt,
.supp-premium-status.status-erhalten { background: rgba(52,211,153,.15); color: #5eeaa7; }
.supp-premium-status.status-problem { background: rgba(217,74,92,.15); color: #ff9a9a; }
.supp-redeem-form { display: none; margin-top: .9rem; padding: 1.1rem; border-radius: 12px; background: rgba(255,255,255,.03); border: 1px solid rgba(212,175,55,.25); }
.supp-redeem-form.offen { display: block; animation: supp-form-in .2s ease-out; }
@keyframes supp-form-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.supp-redeem-form .abo-field { margin-bottom: .8rem; }
.supp-redeem-form label { display: block; font-size: .82rem; color: var(--text-muted); margin-bottom: .35rem; }
.supp-redeem-form input, .supp-redeem-form select {
width: 100%; box-sizing: border-box; padding: .7rem .9rem; border-radius: 9px;
background: rgba(255,255,255,.04); border: 1.5px solid var(--border); color: var(--text);
transition: border-color .15s ease, box-shadow .15s ease;
}
.supp-redeem-form input:focus, .supp-redeem-form select:focus {
outline: none; border-color: rgba(212,175,55,.7); box-shadow: 0 0 0 3px rgba(212,175,55,.14);
}
.supp-checkbox-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem; font-size: .85rem; color: var(--text-muted); }
.supp-checkbox-row input { width: auto; }
/* ---------- Abo verwalten ---------- */
#supp-root .abo-field label { display: block; font-size: .82rem; font-weight: 600; color: var(--text-muted); margin-bottom: .4rem; }
#supp-root .abo-field input {
transition: border-color .15s ease, box-shadow .15s ease;
}
#supp-root .abo-field input:focus {
outline: none; border-color: rgba(212,175,55,.7); box-shadow: 0 0 0 3px rgba(212,175,55,.14);
}
</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');">
<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="container">
<span class="eyebrow" data-i18n="su_eyebrow">👑 Supporter-Bereich</span>
<h1 id="supp-begruessung" data-i18n="su_lade">Dein Supporter-Bereich</h1>
<p class="lead worlds" data-i18n="su_lead">Danke, dass du Dogfather dauerhaft unterstützt.</p>
</div>
</section>
<section class="section-tight" id="supp-root" style="display:none;">
<div class="container">
<div id="supp-kuendigungs-hinweis" class="supp-hinweis" style="display:none;"></div>
<h2 data-i18n="su_fenster1_h2">Mein Abo</h2>
<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>
<div class="supp-stat"><span class="supp-stat-icon" aria-hidden="true">💳</span><div><div class="label" data-i18n="su_stat_preis">Monatlicher Preis</div><div class="value" id="stat-preis"></div></div></div>
<div class="supp-stat supp-stat-clickable" id="stat-card-naechste" tabindex="0" role="button" aria-haspopup="dialog"><span class="supp-stat-icon" aria-hidden="true">📅</span><div><div class="label" data-i18n="su_stat_naechste_zahlung">Nächste Zahlung</div><div class="value" id="stat-naechste"></div><div class="supp-stat-hinweis" data-i18n="su_stat_details_hinweis">Details ansehen</div></div></div>
<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>
<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>
</div>
<h2 style="margin-top:2.4rem;" data-i18n="su_fenster2_h2">Mein aktueller Fortschritt</h2>
<div class="card supp-card">
<p class="small" id="fortschritt-text"></p>
<div class="supp-zeitleiste" id="supp-zeitleiste"></div>
</div>
<h2 style="margin-top:2.4rem;" data-i18n="su_archiv_h2">Mein Sammlungsarchiv</h2>
<div id="supp-archiv"></div>
<h2 style="margin-top:2.4rem;" data-i18n="su_verwalten_h2">Abo verwalten</h2>
<div class="card supp-card">
<div class="abo-field" style="margin-bottom:1rem;">
<label for="edit-name" data-i18n="su_feld_name">Name / Anzeigename</label>
<input type="text" id="edit-name" style="width:100%;padding:.7rem 1rem;border-radius:10px;background:rgba(255,255,255,.04);border:1.5px solid var(--border);color:var(--text);box-sizing:border-box;" />
</div>
<div class="abo-field" style="margin-bottom:1rem;">
<label for="edit-tiktok" data-i18n="su_feld_tiktok">TikTok-Benutzername</label>
<input type="text" id="edit-tiktok" style="width:100%;padding:.7rem 1rem;border-radius:10px;background:rgba(255,255,255,.04);border:1.5px solid var(--border);color:var(--text);box-sizing:border-box;" />
</div>
<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>
<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>
</div>
</section>
<section class="section-tight" id="supp-nicht-eingeloggt" style="display:none;">
<div class="container text-center">
<p class="lead" data-i18n="su_nicht_eingeloggt">Du bist nicht eingeloggt.</p>
<a class="btn btn-primary" href="abonnieren.html" data-i18n="su_zum_abo">Zur DogiCrew 🩵</a>
</div>
</section>
<!-- Detail-Modal für die klickbaren Kennzahl-Kärtchen (Zahlungsdetails /
Fortschritts-Reise) — ein einziger Container, Inhalt wird per JS
ausgetauscht (siehe supp-modal-body). -->
<div class="supp-modal-overlay" id="supp-modal-overlay" hidden>
<div class="supp-modal" id="supp-modal" role="dialog" aria-modal="true" aria-labelledby="supp-modal-titel">
<button type="button" class="supp-modal-close" id="supp-modal-close" aria-label="Schließen"></button>
<div id="supp-modal-body"></div>
</div>
</div>
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-supporter.js"></script>
<script src="assets/js/supporter.js"></script>
<script src="assets/js/main.js"></script>
<script>
(function () {
const S = window.DogiSupporter;
const REDEEM_LABELS = {
teddy_klein: "Kleiner Hasen-Teddy", teddy_mittel: "Mittelgroßer Hasen-Teddy", teddy_gross: "Großer Hasen-Teddy",
tasse_karte: "Tasse & Autogrammkarte", hoodie: "Exklusiver Hoodie",
};
const STATUS_LABELS = {
active: "Aktiv", pending: "Zahlung ausstehend", suspended: "Zahlung fehlgeschlagen",
cancelled: "Gekündigt (bis Periodenende aktiv)", expired: "Beendet", none: "Kein Abo",
};
const REDEEM_STATUS_LABELS = {
verfuegbar: "Verfügbar", eingeloest: "Eingelöst", wird_geprueft: "Wird geprüft", wird_vorbereitet: "Wird vorbereitet",
versandbereit: "Versandbereit", versendet: "Versendet", zugestellt: "Zugestellt", erhalten: "Erhalten",
problem: "Problem gemeldet", ersatz_wird_vorbereitet: "Ersatz wird vorbereitet",
};
const PAYMENT_STATUS_LABELS = {
completed: "Erfolgreich", failed: "Fehlgeschlagen", refunded: "Erstattet", reversed: "Storniert",
};
// Merkt sich die zuletzt geladenen Dashboard-Daten, damit die Detail-
// Modals (Klick auf "Nächste Zahlung" / "Bezahlte Monate gesamt") ohne
// erneuten API-Aufruf sofort befüllt werden können.
let letzteDaten = null;
function fmtDatum(iso) {
if (!iso) return "";
try { return new Date(iso).toLocaleDateString("de-DE"); } catch { return iso; }
}
/* Vorschau-Modus (nur für Dogi/VanVan via gate-worker.js-Rollen-Cookie,
siehe main.js dogiVorschauModus): "Als Abonnent" zeigt diese Seite mit
erfundenen Beispieldaten, OHNE echten Login/Abo — sonst müsste einer
der beiden extra ein echtes Abo abschließen, nur um das Design zu
sehen. "Als Nicht-Abonnent" simuliert stattdessen den ausgeloggten
Zustand, auch wenn zufällig ein echtes Test-Token im Speicher liegt.
Nutzer-Feedback 03.08.2026: "ich will in der Vorschau alles sehen wie
es fertig sein wird, nix da stehen haben von Vorschau, das bringt mir
nix." — die Beispieldaten dürfen also nirgends das Wort "Vorschau"
zeigen, sondern müssen aussehen wie ein echtes, fertiges Konto. Name
und TikTok-Handle richten sich deshalb nach der eingeloggten Rolle
(Dogi/VanVan, window.dogiVorschauName() aus main.js). */
function mockDaten() {
const heute = new Date();
const inTagen = (n) => new Date(heute.getTime() + n * 86400000).toISOString();
const istVanvan = window.dogiVorschauModus && document.cookie.includes("dogi_role=vanvan");
const name = window.dogiVorschauName ? window.dogiVorschauName() : "Dogi";
return {
profile: {
displayName: name,
tiktokUsername: istVanvan ? "o.o.vanessa.o.o" : "dogfather0804",
hasShippingAddress: false,
},
subscription: {
status: "active", monthlyPrice: "4,99 €", nextPaymentAt: inTagen(14),
totalPaidMonths: 8, startedAt: inTagen(-7 * 30 - 4),
},
progress: { cycleNumber: 1, completedCycles: 0, currentCycleMonth: 8 },
// 8 monatliche Zahlungen, neueste zuerst — dieselbe Reihenfolge wie
// das Backend sie liefert (ORDER BY paid_at DESC).
payments: Array.from({ length: 8 }, (_, i) => ({
amount: "4.99", currency: "EUR", payment_status: "completed", paid_at: inTagen(-i * 30), is_sandbox: 0,
})),
timeline: {
cycleNumber: 1,
currentCycleMonth: 8,
milestones: [
{ month: 6, type: "teddy_klein", reached: true, isNext: false, monthsRemaining: 0 },
{ month: 12, type: "teddy_mittel", reached: false, isNext: true, monthsRemaining: 4 },
{ month: 18, type: "teddy_gross", reached: false, isNext: false, monthsRemaining: 10 },
{ month: 24, type: "tasse_karte", reached: false, isNext: false, monthsRemaining: 16 },
{ month: 30, type: "hoodie", reached: false, isNext: false, monthsRemaining: 22 },
],
},
archive: [
{
collectionNumber: 1,
collectionName: "Hasen-Teddy-Kollektion",
premiums: [
{ id: "vorschau-1", label: "Kleiner Hasen-Teddy", redeem_status: "verfuegbar", premium_type: "teddy_klein" },
],
},
],
};
}
async function laden() {
const vorschau = window.dogiVorschauModus ? window.dogiVorschauModus() : null;
// Sichtbarkeit zurücksetzen — laden() kann bei einem Vorschau-Wechsel
// (siehe Event-Listener unten) mehrfach laufen, nicht nur beim ersten
// Seitenaufbau.
document.getElementById("supp-root").style.display = "none";
document.getElementById("supp-nicht-eingeloggt").style.display = "none";
if (vorschau === "nicht-abonnent") {
document.getElementById("supp-nicht-eingeloggt").style.display = "";
return;
}
// "Als Abonnent" heißt nur: mit erfundenen Beispieldaten anzeigen, WENN
// (noch) kein echtes Konto existiert. Ist Dogi/VanVan zufällig wirklich
// eingeloggt, sollen die eigenen echten Daten (echter Name, echter
// Fortschritt) zu sehen sein statt Fantasiewerten — Nutzer-Feedback
// 03.08.2026: "da soll der Name vom Abonnenten stehen, wenn die Abo
// haben." Nur ohne echtes Token braucht es überhaupt die Mock-Daten,
// um das Design vorab begutachten zu können.
const nurMockNoetig = vorschau === "abonnent" && !S.getToken();
let data;
if (nurMockNoetig) {
data = mockDaten();
} else {
if (!S.getToken()) {
document.getElementById("supp-nicht-eingeloggt").style.display = "";
return;
}
try {
data = await S.apiFetch("/supporter/me", {}, true);
} catch {
S.clearToken();
document.getElementById("supp-nicht-eingeloggt").style.display = "";
return;
}
S.cacheSubStatus(data.subscription?.status || "none");
S.cacheDisplayName(data.profile?.displayName || "");
}
letzteDaten = data;
document.getElementById("supp-root").style.display = "";
document.getElementById("supp-begruessung").textContent = `Willkommen, ${data.profile.displayName}!`;
const sub = data.subscription;
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;
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";
if (sub.status === "cancelled") {
const hinweis = document.getElementById("supp-kuendigungs-hinweis");
hinweis.style.display = "";
hinweis.textContent = `Dein Abo wurde gekündigt. Dein Zugang und dein Fortschritt bleiben bis ${fmtDatum(sub.paidPeriodEndAt)} erhalten.`;
}
// Fortschritt-Zeitleiste (Fenster 2)
const t = data.timeline;
document.getElementById("fortschritt-text").textContent =
`Zyklus ${t.cycleNumber} — Monat ${t.currentCycleMonth} von 30`;
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";
return `<div class="supp-meilenstein ${klasse}">
<div class="icon-medaille">${m.reached ? "🎁" : m.isNext ? "✨" : "🔒"}</div>
<div class="monat">Monat ${m.month}</div>
<div class="titel">${REDEEM_LABELS[m.type]}</div>
<div class="supp-premium-status">${status}</div>
</div>`;
})
.join("");
// Sammlungsarchiv
const archivEl = document.getElementById("supp-archiv");
if (!data.archive.length) {
archivEl.innerHTML = `<p class="small" style="opacity:.7;">Noch keine Prämien freigeschaltet — dein erster Meilenstein ist nach 6 erfolgreich bezahlten Monaten erreicht.</p>`;
} else {
archivEl.innerHTML = data.archive
.map(
(kol) => `
<div class="supp-kollektion">
<h3>Supporter-Kollektion ${String(kol.collectionNumber).padStart(2, "0")}${kol.collectionName}</h3>
${kol.premiums
.map(
(p) => `
<div class="supp-premium-row">
<div>
<strong>${p.label}</strong>
<div class="supp-premium-status status-${p.redeem_status}">${REDEEM_STATUS_LABELS[p.redeem_status] || p.redeem_status}${p.tracking_number ? ` · Sendungsnr. ${p.tracking_number}` : ""}</div>
</div>
${
p.redeem_status === "verfuegbar"
? `<button type="button" class="btn btn-outline btn-sm" data-redeem-toggle="${p.id}">Prämie einlösen</button>`
: ""
}
</div>
${p.redeem_status === "verfuegbar" ? redeemFormHtml(p) : ""}
`
)
.join("")}
</div>`
)
.join("");
archivEl.querySelectorAll("[data-redeem-toggle]").forEach((btn) => {
btn.addEventListener("click", () => {
const form = document.getElementById(`redeem-form-${btn.dataset.redeemToggle}`);
form.classList.toggle("offen");
});
});
archivEl.querySelectorAll("[data-redeem-submit]").forEach((btn) => {
btn.addEventListener("click", () => redeemAbschicken(btn.dataset.redeemSubmit, data.profile.hasShippingAddress));
});
}
// Profil-Formular vorbefüllen
document.getElementById("edit-name").value = data.profile.displayName || "";
document.getElementById("edit-tiktok").value = data.profile.tiktokUsername || "";
}
/* ---------- Detail-Modals für die klickbaren Kennzahl-Kärtchen ----------
Nutzer-Wunsch 04.08.2026: "wenn man auf diese 2 Kisten drückt, sollen
die viel mehr Infos kriegen ... richtig geil". Recherche, was für
Infos ein Supporter-/Abo-Bereich hier sinnvollerweise zeigen kann
(analog zu Patreon/Twitch-Sub-Verwaltung & App-Store-Aboverwaltung):
vollständige Zahlungshistorie mit Beträgen/Status, Gesamtsumme,
Zahlungsmethode, Vertragsbeginn — bzw. bei der Fortschritts-Kachel
eine visuelle Fortschrittsleiste zum nächsten Meilenstein, alle
Meilensteine des laufenden Zyklus mit Restzeit, und eine Würdigung
bereits komplett durchlaufener Kollektionen. */
function oeffneModal(html) {
// Verteidigung gegen ein leeres/kaputtes Modal (Nutzer-Meldung
// 04.08.2026: "immer noch" ein leerer Balken statt Inhalt) — falls die
// Builder-Funktion aus irgendeinem Grund nichts Sinnvolles liefert,
// lieber gar nicht erst öffnen als eine leere Hülle zeigen.
if (!html || !String(html).trim()) {
console.error("oeffneModal() wurde mit leerem Inhalt aufgerufen — Modal bleibt zu.");
return;
}
document.getElementById("supp-modal-body").innerHTML = html;
document.getElementById("supp-modal-overlay").hidden = false;
document.body.style.overflow = "hidden";
document.getElementById("supp-modal-close").focus();
}
function schliesseModal() {
document.getElementById("supp-modal-overlay").hidden = true;
document.body.style.overflow = "";
}
document.getElementById("supp-modal-close").addEventListener("click", schliesseModal);
document.getElementById("supp-modal-overlay").addEventListener("click", (e) => {
if (e.target.id === "supp-modal-overlay") schliesseModal();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !document.getElementById("supp-modal-overlay").hidden) schliesseModal();
});
function fmtBetrag(amount, currency) {
const n = parseFloat(amount);
if (Number.isNaN(n)) return "";
return `${n.toFixed(2).replace(".", ",")} ${currency || "EUR"}`;
}
function modalZahlungHtml(data) {
const sub = data.subscription;
const payments = data.payments || [];
const gesamtBezahlt = payments
.filter((p) => p.payment_status === "completed")
.reduce((summe, p) => summe + (parseFloat(p.amount) || 0), 0);
const zeilen = payments.length
? payments
.map(
(p) => `
<div class="supp-pay-row">
<span class="supp-pay-datum">${fmtDatum(p.paid_at)}</span>
<span class="supp-pay-betrag">${fmtBetrag(p.amount, p.currency)}</span>
<span class="supp-pay-status status-${p.payment_status}">${PAYMENT_STATUS_LABELS[p.payment_status] || p.payment_status}</span>
</div>`
)
.join("")
: `<p class="small" style="opacity:.7;">Noch keine Zahlungen erfasst.</p>`;
return `
<div class="supp-modal-head">
<span class="supp-modal-icon" aria-hidden="true">💳</span>
<h3 id="supp-modal-titel">Zahlungsdetails</h3>
<span class="supp-badge ${sub.status}">${STATUS_LABELS[sub.status] || sub.status}</span>
</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>
<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>` : ""}
${sub.paidPeriodEndAt ? `<div><div class="supp-modal-label">Zugang bis</div><div class="supp-modal-value">${fmtDatum(sub.paidPeriodEndAt)}</div></div>` : ""}
<div><div class="supp-modal-label">Insgesamt bezahlt</div><div class="supp-modal-value">${gesamtBezahlt.toFixed(2).replace(".", ",")} €</div></div>
</div>
<h4 class="supp-modal-subh">Zahlungshistorie</h4>
<div class="supp-pay-liste">${zeilen}</div>
`;
}
function modalFortschrittHtml(data) {
const t = data.timeline;
const p = data.progress;
const naechster = t.milestones.find((m) => m.isNext);
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";
return `<div class="supp-modal-meilenstein ${klasse}">
<span class="icon-medaille-mini" aria-hidden="true">${m.reached ? "🎁" : m.isNext ? "✨" : "🔒"}</span>
<div>
<div class="titel">${REDEEM_LABELS[m.type]}</div>
<div class="small" style="opacity:.75;">Monat ${m.month} · ${status}</div>
</div>
</div>`;
})
.join("");
return `
<div class="supp-modal-head">
<span class="supp-modal-icon" aria-hidden="true">🗓️</span>
<h3 id="supp-modal-titel">Deine Reise als Supporter</h3>
</div>
<div class="supp-modal-hero-zahl">${data.subscription.totalPaidMonths}<span class="small">Monate insgesamt treu dabei</span></div>
<div class="supp-progress-bar"><div class="supp-progress-fill" style="width:${prozent}%"></div></div>
<p class="small" style="margin-top:.5rem;opacity:.8;">Zyklus ${t.cycleNumber} — Monat ${t.currentCycleMonth} von 30 (${prozent}%)</p>
${
p.completedCycles > 0
? `<p class="supp-modal-celebrate">🏆 Du hast bereits ${p.completedCycles} komplette Kollektion${p.completedCycles > 1 ? "en" : ""} durchlaufen!</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>`
}
<h4 class="supp-modal-subh">Meilensteine in diesem Zyklus</h4>
<div class="supp-modal-meilensteine">${meilensteineHtml}</div>
`;
}
function bindeStatKarten() {
const naechsteCard = document.getElementById("stat-card-naechste");
const monateCard = document.getElementById("stat-card-monate");
const oeffnen = (builder) => {
// Optional Chaining statt letzteDaten.subscription.status — falls
// letzteDaten aus irgendeinem Grund (Timing, unvollständige
// Vorschau-Daten) kein subscription-Objekt hat, würde der direkte
// Zugriff eine Exception werfen, die den Klick-Handler abbricht,
// OHNE dass irgendwo ein Fehler sichtbar wird — genau das Bild, das
// Dogi als "leeres Modal" gemeldet hat, könnte durch einen
// fehlgeschlagenen Aufruf VOR diesem Fix entstanden sein, wenn eine
// 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;
try {
oeffneModal(builder(letzteDaten));
} catch (err) {
console.error("Konnte Detail-Modal nicht bauen:", err);
}
};
const alsTaste = (fn) => (e) => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fn(); }
};
naechsteCard.addEventListener("click", () => oeffnen(modalZahlungHtml));
naechsteCard.addEventListener("keydown", alsTaste(() => oeffnen(modalZahlungHtml)));
monateCard.addEventListener("click", () => oeffnen(modalFortschrittHtml));
monateCard.addEventListener("keydown", alsTaste(() => oeffnen(modalFortschrittHtml)));
}
bindeStatKarten();
function redeemFormHtml(p) {
return `
<div class="supp-redeem-form" id="redeem-form-${p.id}">
<div class="supp-checkbox-row">
<input type="checkbox" id="use-saved-${p.id}" />
<label for="use-saved-${p.id}" style="margin:0;">Gespeicherte Adresse verwenden</label>
</div>
<div class="abo-field"><label>Name</label><input type="text" id="ship-name-${p.id}" /></div>
<div class="abo-field"><label>Straße &amp; Hausnummer</label><input type="text" id="ship-street-${p.id}" /></div>
<div class="abo-field"><label>PLZ</label><input type="text" id="ship-zip-${p.id}" /></div>
<div class="abo-field"><label>Ort</label><input type="text" id="ship-city-${p.id}" /></div>
<div class="abo-field"><label>Land</label><input type="text" id="ship-country-${p.id}" /></div>
${
p.premium_type === "hoodie"
? `<div class="abo-field"><label>Hoodie-Größe</label>
<select id="hoodie-size-${p.id}">
<option value="S">S</option><option value="M">M</option><option value="L" selected>L</option>
<option value="XL">XL</option><option value="XXL">XXL</option>
</select></div>`
: ""
}
<div class="supp-checkbox-row">
<input type="checkbox" id="save-address-${p.id}" />
<label for="save-address-${p.id}" style="margin:0;">Diese Adresse sicher für zukünftige Prämien speichern</label>
</div>
<button type="button" class="btn btn-primary btn-sm" data-redeem-submit="${p.id}">Absenden</button>
<p class="abo-fehler" id="redeem-fehler-${p.id}"></p>
</div>`;
}
async function redeemAbschicken(premiumId) {
const fehlerEl = document.getElementById(`redeem-fehler-${premiumId}`);
fehlerEl.classList.remove("zeige");
const useSaved = document.getElementById(`use-saved-${premiumId}`).checked;
const body = {
premiumId,
useSavedAddress: useSaved,
shipName: document.getElementById(`ship-name-${premiumId}`).value.trim(),
shipStreet: document.getElementById(`ship-street-${premiumId}`).value.trim(),
shipZip: document.getElementById(`ship-zip-${premiumId}`).value.trim(),
shipCity: document.getElementById(`ship-city-${premiumId}`).value.trim(),
shipCountry: document.getElementById(`ship-country-${premiumId}`).value.trim(),
saveAddress: document.getElementById(`save-address-${premiumId}`).checked,
};
const sizeEl = document.getElementById(`hoodie-size-${premiumId}`);
if (sizeEl) body.hoodieSize = sizeEl.value;
try {
await S.apiFetch("/supporter/redeem-premium", body, true);
await laden();
} catch (err) {
fehlerEl.textContent = err.message;
fehlerEl.classList.add("zeige");
}
}
document.getElementById("btn-save-profile").addEventListener("click", async () => {
const erfolgEl = document.getElementById("profil-erfolg");
erfolgEl.classList.remove("zeige");
try {
await S.apiFetch(
"/supporter/update-profile",
{ displayName: document.getElementById("edit-name").value.trim(), tiktokUsername: document.getElementById("edit-tiktok").value.trim() },
true
);
erfolgEl.classList.add("zeige");
} catch (err) {
alert(err.message);
}
});
document.getElementById("btn-cancel").addEventListener("click", async () => {
if (!confirm("Abo wirklich kündigen? Dein Zugang bleibt bis zum Ende des bezahlten Zeitraums bestehen.")) return;
const fehlerEl = document.getElementById("verwalten-fehler");
fehlerEl.classList.remove("zeige");
try {
await S.apiFetch("/supporter/paypal/cancel", {}, true);
await laden();
} catch (err) {
fehlerEl.textContent = err.message;
fehlerEl.classList.add("zeige");
}
});
document.getElementById("btn-logout").addEventListener("click", async () => {
try { await S.apiFetch("/supporter/logout", {}, true); } catch { /* egal */ }
S.clearToken();
window.location.href = "abonnieren.html";
});
document.addEventListener("dogi-vorschau-geaendert", laden);
// Bewusst NICHT sofort aufgerufen: main.js übersetzt beim
// "DOMContentLoaded"-Ereignis alle data-i18n-Elemente (auch die
// Begrüßungs-Überschrift, die hier gleich mit dem echten Namen
// überschrieben wird) — würde laden() vorher laufen, überschreibt
// applyTranslations() den echten Namen anschließend wieder mit dem
// generischen Standardtext. main.js patcht addEventListener so, dass
// ein "DOMContentLoaded"-Listener, der NACH dem eigentlichen Ereignis
// registriert wird (z.B. beim nahtlosen Seitenwechsel), sofort
// nachgeholt wird — hier reicht also ein normaler Listener für beide
// Fälle (erster Seitenaufbau UND spätere SPA-Navigation).
document.addEventListener("DOMContentLoaded", laden);
})();
</script>
</body>
</html>