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