Files
dogfather-universe/supporter.html
T

868 lines
47 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>