868 lines
47 KiB
HTML
868 lines
47 KiB
HTML
<!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 & 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>
|