Compare commits

..
2 Commits
Author SHA1 Message Date
DogFatherGitandClaude Opus 5 5a5912636d Registrierung: E-Mail-Bestätigung per Einmalcode vor dem Zugangscode
Nutzer-Wunsch 21.08.2026: "bei der ersten registrierung sollen die eine
email bekommen mit einem einmaligen code damit wir auch wissen dass
email stimmt und danach wenn sie den code eingegeben haben sollen die
erst ihren zugangscode auswählen/eintippen können."

Damit wird eine echte Lücke geschlossen: registerSupporter() hat bisher
`email_verified = 1` gesetzt, OHNE dass irgendetwas geprüft wurde -- man
konnte sich mit einer fremden oder erfundenen Adresse registrieren und
kam sofort rein.

Neuer Ablauf in drei Schritten:
1. Name/TikTok/E-Mail -> Konto wird als UNBESTÄTIGT angelegt
   (email_verified = 0, noch kein Zugangscode). Es kommt bewusst KEIN
   Session-Token zurück -- eingeloggt ist man hier noch nicht.
2. Einmalcode aus der E-Mail eingeben -> verify-email bestätigt und
   loggt ein. Die Willkommens-Mail wandert hierher, sie ging vorher an
   eine noch ungeprüfte Adresse.
3. Erst jetzt den eigenen Zugangscode festlegen.

Serverseitig abgesichert: setSupporterAccessCode() lehnt ab, solange die
E-Mail nicht bestätigt ist -- der Schritt ist damit nicht nur im
Formular versteckt, sondern auch per Direktaufruf nicht überspringbar.

Wiederverwendet wird die bereits vorhandene Mechanik (issueAuthCode/
verifyAuthCode mit Zweck "verify_email", sendVerifyEmailCode, die Panels
#panel-code und #panel-neuer-code) -- der "Code vergessen?"-Weg nutzt
dieselbe Code-Eingabe und bleibt unverändert; eine neue Variable
codeZweck unterscheidet, welcher Endpunkt aufgerufen wird.

Mit Playwright end-to-end geprüft (14 Tests): Reihenfolge der Aufrufe,
kein Token vor der Bestätigung, falscher Code kommt nicht weiter,
Zugangscode-Feld erst im letzten Schritt, "Code vergessen?" unberührt.

Cache-Busting-Version auf 20260821o erhöht.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 17:48:08 +02:00
DogFatherGitandClaude Opus 5 5d0ea11570 Arbeit der parallelen Session in git gesichert (war nur auf dem Server)
Diese Änderungen liefen bereits live auf dem Server, lagen dort aber
ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy
(stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen.
Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird.

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

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 17:35:14 +02:00
92 changed files with 1270 additions and 372 deletions
+6 -6
View File
@@ -18,10 +18,10 @@
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<title>Seite nicht gefunden — DOGFATHER UNIVERSE</title> <title>Seite nicht gefunden — DOGFATHER UNIVERSE</title>
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -41,7 +41,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-404.js?v=20260821n"></script> <script src="assets/js/i18n-404.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+291 -86
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Unterstütze Dogfather dauerhaft mit der DogiCrew 🩵 und sammle exklusive Treueprämien — die Teddy-Kollektion entsteht in Kooperation mit VanVan." /> <meta name="twitter:description" content="Unterstütze Dogfather dauerhaft mit der DogiCrew 🩵 und sammle exklusive Treueprämien — die Teddy-Kollektion entsteht in Kooperation mit VanVan." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* ===================================================================== /* =====================================================================
Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback
@@ -216,6 +216,104 @@
.abo-switch a { color: var(--accent); cursor: pointer; font-weight: 600; } .abo-switch a { color: var(--accent); cursor: pointer; font-weight: 600; }
.abo-switch a:hover { text-decoration: underline; } .abo-switch a:hover { text-decoration: underline; }
/* Registrieren/Einloggen-Umschalter (21.08.2026, Nutzer-Wunsch: "die
sollen dann sofort immer den anmelde knopf auswählen können") — zwei
Segmente in einer Pille, aktiver Tab in Gold hervorgehoben, damit die
Wahl sofort ins Auge fällt, noch bevor überhaupt ein Formularfeld zu
sehen ist. Augenschonend: kein grelles Weiß, ruhiger Farbwechsel. */
/* =====================================================================
Aktionsleiste (21.08.2026) — EIN Element für beide Wege, statt vorher
doppeltem "Registrieren" (Umschalter oben + Knopf unten). Links die
eigentliche Aktion in Gold (unverändert im bestehenden Marken-Look),
rechts der Wechsel in Babyblau — der zweiten Hausfarbe der Seite
(theme-dogfather.css). Beide Hälften nutzen dieselbe Technik wie die
bestehenden Gold-Knöpfe (wandernder Verlauf per background-position +
ein durchlaufender Glanzstreifen als ::after) — dadurch wirken sie wie
ein zusammengehöriges Paar statt wie zwei fremde Knöpfe.
Augenschonend (Projektregel): kein Blinken/Flackern, nur ruhig
wandernde Verläufe, und bei prefers-reduced-motion steht alles still.
===================================================================== */
.abo-aktionsleiste {
display: flex; gap: .6rem; margin-top: .4rem;
}
/* flex-basis "auto" statt "0": jede Hälfte startet bei ihrer eigenen
Textbreite und wächst dann anteilig mit. Dadurch bekommt automatisch
die Seite mit dem längeren Text mehr Platz — egal ob das gerade links
("Login-Code anfordern") oder rechts ("Schon registriert? Einloggen")
ist. Mit fester 0-Basis wären beide exakt gleich breit gewesen und der
jeweils längere Text wäre auf zwei Zeilen umgebrochen. */
.abo-aktionsleiste > button {
position: relative; isolation: isolate; overflow: hidden;
flex: 1 1 auto; min-width: 0;
display: flex; align-items: center; justify-content: center; text-align: center;
padding: 1rem .95rem; border-radius: 14px;
font-weight: 800; font-size: .92rem; line-height: 1.25;
white-space: nowrap;
cursor: pointer;
transition: transform .2s ease, box-shadow .2s ease;
}
.abo-aktionsleiste > button::after {
content: ""; position: absolute; inset: 0; z-index: 1;
background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.85) 48%, transparent 66%);
background-size: 240% 100%;
mix-blend-mode: overlay;
animation: abo-btn-glare 3.2s ease-in-out infinite;
pointer-events: none;
}
.abo-aktionsleiste > button:hover { transform: translateY(-3px) scale(1.015); }
/* Links: Gold — exakt die bestehende Knopf-Optik der Seite. */
.abo-aktion-haupt {
color: #1a1408;
border: 1.5px solid rgba(255,233,168,.65);
background: linear-gradient(120deg, #ffe9a8 0%, #d4af37 30%, #ffe9a8 55%, #fff2c9 78%, #d4af37 100%);
background-size: 280% 100%;
animation: abo-btn-shine 6s ease-in-out infinite;
box-shadow: 0 10px 26px -12px rgba(212,175,55,.55);
}
.abo-aktion-haupt:hover {
box-shadow: 0 16px 34px -10px rgba(212,175,55,.65), 0 0 26px rgba(212,175,55,.4);
}
/* Babyblau — gleiche Machart, kräftigeres Sky-Blau statt des sehr
blassen Standard-Babyblaus, damit es neben dem Gold nicht untergeht
(dieselbe Lektion wie auf der Verwaltungsseite). Dunkler Tiefsee-Text
darauf für klaren Kontrast; zusätzlich ein sanft atmender Ring, der die
Kachel leicht leuchten lässt, ohne zu blenden.
Zwei Namen, eine Optik: ".abo-aktion-neben" ist der Wechsel-Knopf neben
einer Gold-Aktion (Registrierungs-Seite), ".abo-aktion-blau" ist ein
Knopf, der selbst DIE Hauptaktion ist und trotzdem blau sein soll
(Einloggen-Seite, Nutzer-Wunsch 21.08.2026). Bewusst getrennte Namen,
damit später klar bleibt, warum ein Knopf blau ist. */
.abo-aktion-neben,
.abo-aktion-blau {
color: #05202b;
border: 1.5px solid rgba(224,247,254,.7);
background: linear-gradient(120deg, #e0f7fe 0%, #38bdf8 30%, #7dd3fc 55%, #f0fbff 78%, #38bdf8 100%);
background-size: 280% 100%;
animation: abo-btn-shine 6s ease-in-out infinite, abo-blau-atem 4.5s ease-in-out infinite;
box-shadow: 0 10px 26px -12px rgba(56,189,248,.6), 0 0 0 0 rgba(125,211,252,.5);
}
.abo-aktion-neben:hover,
.abo-aktion-blau:hover {
box-shadow: 0 16px 34px -10px rgba(56,189,248,.75), 0 0 30px rgba(125,211,252,.55);
}
@keyframes abo-blau-atem {
0%, 100% { box-shadow: 0 10px 26px -12px rgba(56,189,248,.55), 0 0 14px rgba(125,211,252,.25); }
50% { box-shadow: 0 12px 30px -12px rgba(56,189,248,.75), 0 0 26px rgba(125,211,252,.5); }
}
@media (prefers-reduced-motion: reduce) {
.abo-aktionsleiste > button,
.abo-aktionsleiste > button::after { animation: none; }
}
/* Auf schmalen Handys untereinander statt gequetscht nebeneinander. */
@media (max-width: 430px) {
.abo-aktionsleiste { flex-direction: column; gap: .55rem; }
.abo-aktionsleiste > button { font-size: .92rem; }
}
.abo-oder { .abo-oder {
display: flex; align-items: center; gap: .8rem; display: flex; align-items: center; gap: .8rem;
color: var(--text-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
@@ -321,62 +419,78 @@
korrekt "nicht bedienbar" meldet. --> korrekt "nicht bedienbar" meldet. -->
<div class="abo-field"> <div class="abo-field">
<label for="reg-name" data-i18n="ab_feld_name">Name oder Anzeigename</label> <label for="reg-name" data-i18n="ab_feld_name">Name oder Anzeigename</label>
<input type="text" id="reg-name" maxlength="60" disabled /> <input type="text" id="reg-name" maxlength="60" />
</div> </div>
<div class="abo-field"> <div class="abo-field">
<label for="reg-tiktok" data-i18n="ab_feld_tiktok">TikTok-Benutzername</label> <label for="reg-tiktok" data-i18n="ab_feld_tiktok">TikTok-Benutzername</label>
<input type="text" id="reg-tiktok" placeholder="@benutzername" maxlength="40" disabled /> <input type="text" id="reg-tiktok" placeholder="@benutzername" maxlength="40" />
</div> </div>
<div class="abo-field"> <div class="abo-field">
<label for="reg-email" data-i18n="ab_feld_email">E-Mail-Adresse</label> <label for="reg-email" data-i18n="ab_feld_email">E-Mail-Adresse</label>
<input type="email" id="reg-email" maxlength="120" disabled /> <input type="email" id="reg-email" maxlength="120" />
</div>
<!-- Das Zugangscode-Feld stand bis 21.08.2026 hier, direkt neben den
Stammdaten. Es ist bewusst in einen eigenen, SPÄTEREN Schritt
gewandert (Nutzer-Wunsch: "bei der ersten registrierung sollen
die eine email bekommen mit einem einmaligen code damit wir auch
wissen dass email stimmt und danach wenn sie den code eingegeben
haben sollen die erst ihren zugangscode auswählen/eintippen
können"). Ablauf jetzt: hier Stammdaten -> Einmalcode per Mail
(#panel-code) -> Zugangscode festlegen (#panel-neuer-code). -->
<p class="small muted" style="margin:-.2rem 0 .2rem;" data-i18n="ab_reg_hinweis_bestaetigung">Wir schicken dir gleich einen 6-stelligen Code per E-Mail. Danach legst du deinen eigenen Zugangscode fest.</p>
<!-- Nutzer-Wunsch 21.08.2026: "die leute sollen sich auch schon
registrieren können, aber der abo soll noch zu sein, da soll
dann coming soon stehen." Registrierung/Login sind ab jetzt
offen -- Konten können angelegt werden. Gesperrt bleibt NUR
noch der eigentliche Zahlungsschritt danach (siehe
panel-subscribe weiter unten), weil ohne Dogis echte PayPal-
Business-Zugangsdaten niemand wirklich zahlen kann. Wieder
normaler .btn-primary-Stil statt .btn-gold-locked. -->
<!-- Aktionsleiste (21.08.2026, Nutzer-Wunsch: "bringt nichts 2 mal
registrieren da zu haben"). EIN Element statt vorher Umschalter
oben + Knopf unten: links die eigentliche Aktion in Gold,
rechts der Wechsel zum Einloggen in Babyblau. Beide Hälften
sind immer aktiv (kein Tab-Zustand) — links macht etwas,
rechts wechselt die Ansicht. -->
<div class="abo-aktionsleiste">
<button type="button" class="abo-aktion-haupt" id="btn-register" data-i18n="ab_btn_register">Registrieren</button>
<button type="button" class="abo-aktion-neben" id="tab-login" data-i18n="ab_tab_login">Schon registriert? Einloggen</button>
</div> </div>
<!-- Bugfix/Nutzer-Wunsch 20.08.2026: Die DogiCrew-Registrierung ist
technisch fertig, aber ohne Dogis echte PayPal-Business-Zugangsdaten
kann niemand wirklich zahlen (siehe DogFather Website - Offene
Punkte.md). Bis die stehen: Knopf bleibt sichtbar (Teaser, "soll
alles bleiben"), aber NICHT bedienbar — natives disabled-Attribut
(nicht nur optisch/pointer-events, damit es auch für Tastatur/
Screenreader korrekt gesperrt ist). main.js hängt weiterhin
unverändert einen Klick-Listener an dieselbe ID — disabled-Buttons
feuern in allen Browsern keine Click-Events, daher hier bewusst
KEINE JS-Änderung nötig. Eigener Gold/Schloss-Stil (.btn-gold-locked,
siehe main.css) statt des generischen .btn-silver, der auf dieser
Seite schon für die Teddy-Kollektion vergeben ist. -->
<button type="button" class="btn-gold-locked" style="width:100%;margin-top:.2rem;" id="btn-register"
disabled aria-disabled="true">
<span class="btn-gold-locked-lock" aria-hidden="true">🔒</span>
<span class="btn-gold-locked-text" data-i18n="ab_kollektion_coming_soon">Coming soon…</span>
</button>
<p class="abo-fehler" id="register-fehler"></p> <p class="abo-fehler" id="register-fehler"></p>
<p class="abo-switch"><a id="switch-to-login" data-i18n="ab_switch_login">Schon Supporter? Hier einloggen</a></p>
</div> </div>
<!-- Zustand 3: Login (E-Mail-Code anfordern) --> <!-- Zustand 3: Einloggen mit E-Mail + eigenem Zugangscode.
<!-- Bugfix/Nutzer-Wunsch 20.08.2026: War die einzige noch offene Lücke, nachdem Umgestellt 21.08.2026 (Nutzer-Wunsch: "wieso sollte ich wieder
der Registrieren-Knopf gesperrt wurde — über "Schon Supporter? Hier ein code bekommen, mach die anmeldung einfach"): vorher wurde
einloggen" kam man hierher und konnte trotzdem einen echten Login-Code auch hier bei JEDEM Login erst ein Einmalcode per E-Mail
anfordern, obwohl die DogiCrew noch gar nicht live ist. Jetzt genauso verschickt — für längst registrierte Leute ein völlig
gesperrt: E-Mail-Feld disabled (nicht eintippbar, wie gewünscht), Knopf unnötiger Umweg. Der E-Mail-Weg existiert weiterhin, aber nur
derselbe .btn-gold-locked-Stil wie beim Registrieren-Knopf. Betrifft NUR noch als "Code vergessen?" (siehe Link unten). -->
diesen öffentlichen Anmelde-Einstieg — bereits eingeloggte Supporter
(Dogi/VanVan als Test-Accounts) merken nichts davon, ihr Zugang läuft
weiterhin über den gespeicherten Token/supporter.html. -->
<div class="abo-panel" id="panel-login" hidden> <div class="abo-panel" id="panel-login" hidden>
<div class="abo-field"> <div class="abo-field">
<label for="login-email" data-i18n="ab_feld_email">E-Mail-Adresse</label> <label for="login-email" data-i18n="ab_feld_email">E-Mail-Adresse</label>
<input type="email" id="login-email" maxlength="120" disabled /> <input type="email" id="login-email" maxlength="120" autocomplete="email" />
</div>
<div class="abo-field">
<label for="login-code" data-i18n="ab_feld_code_eigen">Dein Zugangscode</label>
<input type="password" id="login-code" maxlength="200" autocomplete="current-password" />
</div>
<!-- Nutzer-Wunsch 21.08.2026: "da soll nur noch einloggen sein und
der knopf soll auch blau sein, der neu registriert soll weg."
Hier steht deshalb nur EIN Knopf, in Babyblau statt Gold —
wer auf dieser Ansicht gelandet ist, will einloggen; der Weg
zurück zur Registrierung führt über den Zurück-Knopf des
Browsers bzw. den blauen Knopf auf der Registrierungs-Seite. -->
<div class="abo-aktionsleiste">
<button type="button" class="abo-aktion-blau" id="btn-login" data-i18n="ab_btn_login">Einloggen</button>
</div> </div>
<button type="button" class="btn-gold-locked" style="width:100%;margin-top:.2rem;" id="btn-login-request"
disabled aria-disabled="true">
<span class="btn-gold-locked-lock" aria-hidden="true">🔒</span>
<span class="btn-gold-locked-text" data-i18n="ab_kollektion_coming_soon">Coming soon…</span>
</button>
<p class="abo-fehler" id="login-fehler"></p> <p class="abo-fehler" id="login-fehler"></p>
<p class="abo-switch"><a id="switch-to-register" data-i18n="ab_switch_register">Noch kein Konto? Jetzt registrieren</a></p> <p class="abo-switch"><a id="code-vergessen" data-i18n="ab_code_vergessen">Code vergessen?</a></p>
</div> </div>
<!-- Zustand 4: Code-Eingabe (nach Registrierung ODER Login-Anfrage) --> <!-- Zustand 4: Einmalcode aus der E-Mail eingeben. Kommt seit
21.08.2026 NUR noch im "Code vergessen?"-Weg vor — die normale
Registrierung/Anmeldung braucht ihn nicht mehr. -->
<div class="abo-panel" id="panel-code" hidden> <div class="abo-panel" id="panel-code" hidden>
<p class="small" id="code-hinweis" data-i18n="ab_code_hinweis">Wir haben dir einen 6-stelligen Code per E-Mail geschickt.</p> <p class="small" id="code-hinweis" data-i18n="ab_code_hinweis">Wir haben dir einen 6-stelligen Code per E-Mail geschickt.</p>
<div class="abo-field"> <div class="abo-field">
@@ -387,6 +501,20 @@
<p class="abo-fehler" id="code-fehler"></p> <p class="abo-fehler" id="code-fehler"></p>
</div> </div>
<!-- Zustand 4b: neuen dauerhaften Zugangscode festlegen. Letzter
Schritt des "Code vergessen?"-Weges — an dieser Stelle ist man
durch den Einmalcode bereits eingeloggt, es fehlt nur noch der
neue Code für künftige Anmeldungen. -->
<div class="abo-panel" id="panel-neuer-code" hidden>
<p class="small" data-i18n="ab_neuer_code_hinweis">Fast fertig — leg jetzt deinen neuen Zugangscode fest.</p>
<div class="abo-field">
<label for="neuer-code" data-i18n="ab_feld_code_neu">Dein Zugangscode (mindestens 6 Zeichen)</label>
<input type="password" id="neuer-code" maxlength="200" autocomplete="new-password" />
</div>
<button type="button" class="btn btn-primary" style="width:100%;" id="btn-neuer-code" data-i18n="ab_btn_code_speichern">Zugangscode speichern</button>
<p class="abo-fehler" id="neuer-code-fehler"></p>
</div>
<!-- Zustand 5: eingeloggt, noch kein Abo -> PayPal Smart Buttons. <!-- Zustand 5: eingeloggt, noch kein Abo -> PayPal Smart Buttons.
Zeigt automatisch alle für die Käuferin/den Käufer verfügbaren Zeigt automatisch alle für die Käuferin/den Käufer verfügbaren
Zahlarten (PayPal-Konto, Kreditkarte als Gast, je nach Land) — Zahlarten (PayPal-Konto, Kreditkarte als Gast, je nach Land) —
@@ -394,24 +522,45 @@
<div class="abo-panel" id="panel-subscribe" hidden> <div class="abo-panel" id="panel-subscribe" hidden>
<p class="abo-erfolg zeige" data-i18n="ab_eingeloggt_als">Eingeloggt.</p> <p class="abo-erfolg zeige" data-i18n="ab_eingeloggt_als">Eingeloggt.</p>
<!-- Button-Lösung (§ 312j BGB / entspr. Regelung): Preis- und <!-- Nutzer-Wunsch 21.08.2026: "die leute sollen sich schon registrieren
Zahlungspflicht-Hinweis unmittelbar vor der Zahlung. --> können, aber der abo soll noch zu sein, da soll dann coming soon
<p class="small" style="text-align:center;font-weight:700;margin-bottom:.8rem;">4,99 € / Monat · zahlungspflichtiges Abo · monatlich kündbar</p> stehen." Konto ist angelegt/Login hat geklappt -- aber die eigent-
liche Zahlung (PayPal) bleibt gesperrt, bis Dogis echte PayPal-
Business-Zugangsdaten stehen. Die echten Zahlungs-Elemente (Preis-
Hinweis, Widerrufs-Checkbox, PayPal-Container) bleiben unten UNVER-
ÄNDERT im DOM (nur per hidden versteckt), damit das Skript weiter
unten (Checkbox-Listener, ladePaypalButtons() usw.) unangetastet
funktioniert -- einfach das hidden am Wrapper unten entfernen,
sobald PayPal bereitsteht. -->
<div class="btn-gold-locked" style="width:100%;margin-top:.4rem;cursor:default;">
<span class="btn-gold-locked-lock" aria-hidden="true">🔒</span>
<span class="btn-gold-locked-text" data-i18n="ab_kollektion_coming_soon">Coming soon…</span>
</div>
<p class="small muted" style="text-align:center;margin-top:.9rem;" data-i18n="ab_subscribe_locked_hinweis">Dein Konto ist bereit — die Bezahlung startet, sobald das Abo offiziell live geht. Schau einfach bald wieder vorbei.</p>
<!-- Widerrufsrecht bei digitalen Inhalten (§ 356 Abs. 5 BGB / <!-- Nutzer-Wunsch 21.08.2026: "ich will dass jetzt alles verbunden
entspr. Regelung, siehe agb.html Ziffer 6): ohne aktives ist so dass man mit den klicks pro klick weiter kommt" — vorher
Ankreuzen bleibt der Zahlungsbereich gesperrt, es gibt keine war hier eine Sackgasse (kein Link/Knopf mehr nach dem Login).
Zahlung ohne diese ausdrückliche, nicht vorausgewählte Jetzt zwei klare Wege weiter: zurück zur Startseite, oder
Zustimmung. --> direkt zum Streamplan (naheliegendster nächster Klick für
<label class="abo-widerruf-check" for="widerruf-consent"> jemanden, der sich gerade für Dogi interessiert). -->
<input type="checkbox" id="widerruf-consent" /> <div class="btn-row" style="justify-content:center;margin-top:1.4rem;">
<span>Ich wünsche ausdrücklich, dass der Zugang zum Supporter-Bereich sofort mit Vertragsschluss beginnt, und weiß, dass mein 14-tägiges Widerrufsrecht dadurch erlischt, sobald der Zugang vollständig bereitgestellt ist (Details in den <a href="agb.html" target="_blank" rel="noopener">AGB</a>, Ziffer 6).</span> <a class="btn btn-outline" href="index.html" data-i18n="ab_weiter_startseite">Zur Startseite →</a>
</label> <a class="btn btn-outline" href="streamplan.html" data-i18n="ab_weiter_streamplan">Zum Streamplan →</a>
</div>
<div id="paypal-button-container" class="paypal-gesperrt"></div> <div hidden>
<p class="small muted" id="paypal-gesperrt-hinweis">Bitte bestätige oben die Checkbox, um die Zahlungsoptionen freizuschalten.</p> <p class="small" style="text-align:center;font-weight:700;margin-bottom:.8rem;">4,99 € / Monat · zahlungspflichtiges Abo · monatlich kündbar</p>
<p class="small" id="paypal-laedt" data-i18n="ab_paypal_laedt" hidden>Zahlungsoptionen werden geladen …</p> <label class="abo-widerruf-check" for="widerruf-consent">
<p class="abo-fehler" id="subscribe-fehler"></p> <input type="checkbox" id="widerruf-consent" />
<span>Ich wünsche ausdrücklich, dass der Zugang zum Supporter-Bereich sofort mit Vertragsschluss beginnt, und weiß, dass mein 14-tägiges Widerrufsrecht dadurch erlischt, sobald der Zugang vollständig bereitgestellt ist (Details in den <a href="agb.html" target="_blank" rel="noopener">AGB</a>, Ziffer 6).</span>
</label>
<div id="paypal-button-container" class="paypal-gesperrt"></div>
<p class="small muted" id="paypal-gesperrt-hinweis">Bitte bestätige oben die Checkbox, um die Zahlungsoptionen freizuschalten.</p>
<p class="small" id="paypal-laedt" data-i18n="ab_paypal_laedt" hidden>Zahlungsoptionen werden geladen …</p>
<p class="abo-fehler" id="subscribe-fehler"></p>
</div>
</div> </div>
<!-- Zustand 6: bereits Supporter --> <!-- Zustand 6: bereits Supporter -->
@@ -454,13 +603,13 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-abonnieren.js?v=20260821n"></script> <script src="assets/js/i18n-abonnieren.js?v=20260821o"></script>
<script src="assets/js/supporter.js?v=20260821n"></script> <script src="assets/js/supporter.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
const S = window.DogiSupporter; const S = window.DogiSupporter;
const panels = ["panel-lade", "panel-register", "panel-login", "panel-code", "panel-subscribe", "panel-already"]; const panels = ["panel-lade", "panel-register", "panel-login", "panel-code", "panel-neuer-code", "panel-subscribe", "panel-already"];
// panel-social-login (Google-Button) hängt sich an register/login dran, // panel-social-login (Google-Button) hängt sich an register/login dran,
// ist aber kein "richtiger" exklusiver Zustand — separat gesteuert. // ist aber kein "richtiger" exklusiver Zustand — separat gesteuert.
const SOCIAL_LOGIN_PANELS = ["panel-register", "panel-login"]; const SOCIAL_LOGIN_PANELS = ["panel-register", "panel-login"];
@@ -498,8 +647,14 @@
el.classList.remove("zeige"); el.classList.remove("zeige");
} }
let codePurpose = "verify_email"; // oder "login" // Merkt sich die E-Mail-Adresse zwischen "Code vergessen?" und der
// Eingabe des Einmalcodes (der frühere codePurpose-Schalter entfiel mit
// der Umstellung 21.08.2026 — es gibt nur noch diesen einen Code-Weg).
let codeEmail = ""; let codeEmail = "";
// Merkt sich, WARUM gerade ein Einmalcode eingegeben wird (21.08.2026):
// "register" = Erstbestätigung der E-Mail, "login" = "Code vergessen?".
// Steuert in btn-code-verify, welcher Endpunkt aufgerufen wird.
let codeZweck = "login";
async function pruefeStatus() { async function pruefeStatus() {
if (!S.getToken()) { if (!S.getToken()) {
@@ -511,11 +666,24 @@
const status = data.subscription?.status || "none"; const status = data.subscription?.status || "none";
S.cacheSubStatus(status); S.cacheSubStatus(status);
S.cacheDisplayName(data.profile?.displayName || ""); S.cacheDisplayName(data.profile?.displayName || "");
if (["active", "pending", "suspended", "cancelled"].includes(status)) { /* Nutzer-Wunsch 21.08.2026: "anstatt das was du jetzt auf dem screen
zeige("panel-already"); siehst, sollen die leute nicht mehr sehen wenn sie angemeldet sind.
} else { sie sollen die richtige seite sehen die wir schon mal vorbereitet
zeige("panel-subscribe"); hatten, wo sie ihre persönliche daten sehen und so."
}
Wer eingeloggt ist, gehört nicht mehr auf diese Anmeldeseite —
egal ob mit oder ohne bezahltes Abo. supporter.html zeigt in BEIDEN
Fällen das Richtige: persönliche Daten, Fortschritt mit allen
Prämien-Meilensteinen, Sammlungsarchiv, Profil bearbeiten,
Ausloggen — bei "kein Abo" eben mit entsprechendem Hinweis statt
echter Laufzeit. Vorher landete man hier stattdessen in einer
Sackgasse ("Coming soon…"), ohne je die eigentliche Seite zu sehen.
replace() statt href: die Anmeldeseite verschwindet damit aus dem
Verlauf — ein "Zurück" führt sonst sofort wieder hierher und von
hier wieder weiter, was sich wie eine Schleife anfühlt. */
location.replace("supporter.html");
return;
} catch (err) { } catch (err) {
// Token ungültig/abgelaufen -> zurück zur Registrierung/Login // Token ungültig/abgelaufen -> zurück zur Registrierung/Login
S.clearToken(); S.clearToken();
@@ -628,8 +796,10 @@
ladeHinweis.hidden = true; ladeHinweis.hidden = true;
} }
document.getElementById("switch-to-login").addEventListener("click", () => zeige("panel-login")); // Der frühere Gegenstück-Knopf "Neu registrieren" auf der Einloggen-Seite
document.getElementById("switch-to-register").addEventListener("click", () => zeige("panel-register")); // wurde am 21.08.2026 auf Wunsch entfernt — deshalb hier nur noch der eine
// Wechsel von der Registrierung zum Einloggen.
document.getElementById("tab-login").addEventListener("click", () => zeige("panel-login"));
// Widerrufsrecht-Checkbox: erst nach aktivem Ankreuzen werden die // Widerrufsrecht-Checkbox: erst nach aktivem Ankreuzen werden die
// PayPal-Zahlungsoptionen überhaupt geladen/klickbar (siehe agb.html // PayPal-Zahlungsoptionen überhaupt geladen/klickbar (siehe agb.html
@@ -658,24 +828,44 @@
const email = document.getElementById("reg-email").value.trim(); const email = document.getElementById("reg-email").value.trim();
if (!displayName || !email) return fehlerZeigen(el, "Bitte Name und E-Mail-Adresse angeben."); if (!displayName || !email) return fehlerZeigen(el, "Bitte Name und E-Mail-Adresse angeben.");
try { try {
// Schritt 1 von 3 (21.08.2026): /supporter/register legt nur das noch
// unbestätigte Konto an und verschickt einen Einmalcode -- es kommt
// bewusst KEIN Token zurück, eingeloggt ist man erst nach Schritt 2.
await S.apiFetch("/supporter/register", { displayName, tiktokUsername, email }); await S.apiFetch("/supporter/register", { displayName, tiktokUsername, email });
codePurpose = "verify_email";
codeEmail = email; codeEmail = email;
codeZweck = "register";
zeige("panel-code"); zeige("panel-code");
} catch (err) { } catch (err) {
fehlerZeigen(el, err.message); fehlerZeigen(el, err.message);
} }
}); });
document.getElementById("btn-login-request").addEventListener("click", async () => { // Normale Anmeldung: E-Mail + eigener Zugangscode, ohne Umweg über E-Mail.
document.getElementById("btn-login").addEventListener("click", async () => {
const el = document.getElementById("login-fehler"); const el = document.getElementById("login-fehler");
fehlerVerstecken(el); fehlerVerstecken(el);
const email = document.getElementById("login-email").value.trim(); const email = document.getElementById("login-email").value.trim();
if (!email) return fehlerZeigen(el, "Bitte E-Mail-Adresse angeben."); const accessCode = document.getElementById("login-code").value;
if (!email || !accessCode) return fehlerZeigen(el, "Bitte E-Mail-Adresse und Zugangscode eingeben.");
try {
const data = await S.apiFetch("/supporter/login-code", { email, accessCode });
S.setToken(data.token);
await pruefeStatus();
} catch (err) {
fehlerZeigen(el, err.message);
}
});
// "Code vergessen?" — hier lebt der alte E-Mail-Einmalcode-Weg weiter.
document.getElementById("code-vergessen").addEventListener("click", async () => {
const el = document.getElementById("login-fehler");
fehlerVerstecken(el);
const email = document.getElementById("login-email").value.trim();
if (!email) return fehlerZeigen(el, "Bitte zuerst deine E-Mail-Adresse eingeben.");
try { try {
await S.apiFetch("/supporter/login-request", { email }); await S.apiFetch("/supporter/login-request", { email });
codePurpose = "login";
codeEmail = email; codeEmail = email;
codeZweck = "login";
zeige("panel-code"); zeige("panel-code");
} catch (err) { } catch (err) {
fehlerZeigen(el, err.message); fehlerZeigen(el, err.message);
@@ -688,9 +878,26 @@
const code = document.getElementById("code-input").value.trim(); const code = document.getElementById("code-input").value.trim();
if (!code) return fehlerZeigen(el, "Bitte Code eingeben."); if (!code) return fehlerZeigen(el, "Bitte Code eingeben.");
try { try {
const path = codePurpose === "login" ? "/supporter/verify-login" : "/supporter/verify-email"; // Dieselbe Code-Eingabe bedient zwei Wege (21.08.2026):
const data = await S.apiFetch(path, { email: codeEmail, code }); // "register" -> Erstbestätigung der E-Mail bei der Registrierung
// "login" -> "Code vergessen?"-Weg für bestehende Konten
// Beide enden gleich: eingeloggt, danach Zugangscode festlegen.
const endpunkt = codeZweck === "register" ? "/supporter/verify-email" : "/supporter/verify-login";
const data = await S.apiFetch(endpunkt, { email: codeEmail, code });
S.setToken(data.token); S.setToken(data.token);
zeige("panel-neuer-code");
} catch (err) {
fehlerZeigen(el, err.message);
}
});
document.getElementById("btn-neuer-code").addEventListener("click", async () => {
const el = document.getElementById("neuer-code-fehler");
fehlerVerstecken(el);
const accessCode = document.getElementById("neuer-code").value;
if (!accessCode) return fehlerZeigen(el, "Bitte leg einen Zugangscode fest.");
try {
await S.apiFetch("/supporter/set-access-code", { accessCode }, true);
await pruefeStatus(); await pruefeStatus();
} catch (err) { } catch (err) {
fehlerZeigen(el, err.message); fehlerZeigen(el, err.message);
@@ -705,15 +912,13 @@
} catch { } catch {
/* Konfiguration konnte nicht geladen werden — Google-Button/PayPal-Buttons bleiben dann aus. */ /* Konfiguration konnte nicht geladen werden — Google-Button/PayPal-Buttons bleiben dann aus. */
} }
/* Bugfix/Nutzer-Wunsch 20.08.2026: "Mit Google anmelden" wäre ein dritter, bisher /* Bugfix 20.08.2026, wieder aktiviert 21.08.2026: "Mit Google anmelden" war ein
übersehener Weg gewesen, trotz gesperrtem Registrieren-/Login-Knopf doch ein dritter, bisher übersehener Weg, trotz gesperrtem Registrieren-/Login-Knopf
echtes Konto anzulegen bzw. sich einzuloggen (falls GOOGLE_CLIENT_ID auf dem doch ein echtes Konto anzulegen bzw. sich einzuloggen. War deshalb deaktiviert,
Server konfiguriert ist). initGoogleConsent() daher bewusst NICHT mehr solange die Registrierung selbst gesperrt war. Registrierung ist jetzt (Nutzer-
aufgerufen, solange die DogiCrew-Registrierung gesperrt ist — panel-social-login Wunsch 21.08.2026) bewusst offen -- Google-Login ist nur ein weiterer,
bleibt dadurch dauerhaft hidden. Einfach diesen Aufruf wieder aktivieren, gleichwertiger Weg zum selben jetzt erlaubten Ziel, kein Schlupfloch mehr. */
sobald die Registrierung selbst (btn-register/btn-login-request, siehe oben) initGoogleConsent();
entsperrt wird. */
// initGoogleConsent();
await pruefeStatus(); await pruefeStatus();
} }
+5 -5
View File
@@ -20,10 +20,10 @@
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -134,6 +134,6 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Detailansicht einer Aktion oder eines Projekts von Team Dogi." /> <meta name="twitter:description" content="Detailansicht einer Aktion oder eines Projekts von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -51,9 +51,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-aktionen.js?v=20260821n"></script> <script src="assets/js/data-aktionen.js?v=20260821o"></script>
<script src="assets/js/i18n-aktion-detail.js?v=20260821n"></script> <script src="assets/js/i18n-aktion-detail.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
+6
View File
@@ -1566,6 +1566,12 @@ header.site-header {
.btn-outline:hover { border-color: var(--accent); color: var(--accent); } .btn-outline:hover { border-color: var(--accent); color: var(--accent); }
.btn-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; } .btn-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }
/* Kurzes "✓ Link kopiert"-Feedback für dogiTeilen()-Knöpfe (Zwischenablage-
Fallback ohne Web-Share-API) -- 21.08.2026. Gleiche Erfolgsfarbe wie
.vw-copy-btn.kopiert in der Verwaltung, für ein konsistentes Gefühl
zwischen öffentlicher Seite und Admin-Bereich. */
.dogi-teilen-kopiert { background: rgba(52,211,153,.15) !important; border-color: rgba(52,211,153,.5) !important; color: #5eeaa7 !important; }
/* ---------- Hero ---------- */ /* ---------- Hero ---------- */
.hero { .hero {
position: relative; position: relative;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 356 KiB

+79 -2
View File
@@ -126,10 +126,62 @@ window.I18N_PAGE = {
de: "Registrieren", "de-CH": "Registriere", de: "Registrieren", "de-CH": "Registriere",
en: "Register", fr: "S'inscrire", pt: "Registrar", en: "Register", fr: "S'inscrire", pt: "Registrar",
}, },
ab_tab_register: {
de: "Neu registrieren", "de-CH": "Nöi registriere",
en: "Sign up", fr: "S'inscrire", pt: "Registrar",
},
ab_tab_login: {
de: "Schon registriert? Einloggen", "de-CH": "Scho registriert? Yylogge",
en: "Already registered? Log in", fr: "Déjà inscrit ? Se connecter", pt: "Já registrado? Entrar",
},
ab_switch_login: { ab_switch_login: {
de: "Schon Supporter? Hier einloggen", "de-CH": "Scho Supporter? Da yylogge", de: "Schon Supporter? Hier einloggen", "de-CH": "Scho Supporter? Da yylogge",
en: "Already a supporter? Log in here", fr: "Déjà supporter ? Connecte-toi ici", pt: "Já é apoiador? Faça login aqui", en: "Already a supporter? Log in here", fr: "Déjà supporter ? Connecte-toi ici", pt: "Já é apoiador? Faça login aqui",
}, },
ab_feld_code_neu: {
de: "Denk dir jetzt deinen Zugangscode aus (mindestens 6 Zeichen)",
"de-CH": "Dänk der jetz din Zuegangscode us (mindeschtens 6 Zeiche)",
en: "Now choose your own access code (at least 6 characters)",
fr: "Choisis maintenant ton code d'accès (au moins 6 caractères)",
pt: "Agora escolha seu código de acesso (mínimo 6 caracteres)",
},
ab_feld_code_platzhalter: {
de: "Frei wählbar — z. B. DogiCrew2026",
"de-CH": "Frei wählbar — z. B. DogiCrew2026",
en: "Anything you like — e.g. DogiCrew2026",
fr: "Comme tu veux — p. ex. DogiCrew2026",
pt: "À sua escolha — ex. DogiCrew2026",
},
ab_code_hinweis_neu: {
de: "Mit diesem Code loggst du dich künftig ein — merk ihn dir gut.",
"de-CH": "Mit däm Code loggsch di künftig y — merk der en guet.",
en: "You'll use this code to log in from now on — keep it safe.",
fr: "Tu utiliseras ce code pour te connecter — garde-le en lieu sûr.",
pt: "Você usará este código para entrar a partir de agora — guarde-o bem.",
},
ab_feld_code_eigen: {
de: "Dein Zugangscode", "de-CH": "Din Zuegangscode",
en: "Your access code", fr: "Ton code d'accès", pt: "Seu código de acesso",
},
ab_btn_login: {
de: "Einloggen", "de-CH": "Yylogge",
en: "Log in", fr: "Se connecter", pt: "Entrar",
},
ab_code_vergessen: {
de: "Code vergessen?", "de-CH": "Code vergässe?",
en: "Forgot your code?", fr: "Code oublié ?", pt: "Esqueceu seu código?",
},
ab_neuer_code_hinweis: {
de: "Fast fertig — leg jetzt deinen neuen Zugangscode fest.",
"de-CH": "Fascht fertig — leg jetz din nöie Zuegangscode fescht.",
en: "Almost done — now set your new access code.",
fr: "Presque fini — définis maintenant ton nouveau code d'accès.",
pt: "Quase pronto — agora defina seu novo código de acesso.",
},
ab_btn_code_speichern: {
de: "Zugangscode speichern", "de-CH": "Zuegangscode spichere",
en: "Save access code", fr: "Enregistrer le code d'accès", pt: "Salvar código de acesso",
},
ab_btn_login_code: { ab_btn_login_code: {
de: "Login-Code anfordern", "de-CH": "Login-Code aafordere", de: "Login-Code anfordern", "de-CH": "Login-Code aafordere",
en: "Request login code", fr: "Demander un code de connexion", pt: "Solicitar código de login", en: "Request login code", fr: "Demander un code de connexion", pt: "Solicitar código de login",
@@ -142,6 +194,16 @@ window.I18N_PAGE = {
de: "Wir haben dir einen 6-stelligen Code per E-Mail geschickt.", "de-CH": "Mir händ dir en 6-stellige Code per E-Mail gschickt.", de: "Wir haben dir einen 6-stelligen Code per E-Mail geschickt.", "de-CH": "Mir händ dir en 6-stellige Code per E-Mail gschickt.",
en: "We've sent you a 6-digit code by email.", fr: "Nous t'avons envoyé un code à 6 chiffres par e-mail.", pt: "Enviamos um código de 6 dígitos para o seu e-mail.", en: "We've sent you a 6-digit code by email.", fr: "Nous t'avons envoyé un code à 6 chiffres par e-mail.", pt: "Enviamos um código de 6 dígitos para o seu e-mail.",
}, },
/* Hinweis im Registrierungsformular (21.08.2026): erklärt vorab, dass jetzt
zwei weitere Schritte kommen (Code aus der E-Mail, danach eigener
Zugangscode) -- sonst wirkt der Sprung zur Code-Eingabe überraschend. */
ab_reg_hinweis_bestaetigung: {
de: "Wir schicken dir gleich einen 6-stelligen Code per E-Mail. Danach legst du deinen eigenen Zugangscode fest.",
"de-CH": "Mir schicked dir grad en 6-stellige Code per E-Mail. Denn legsch du din eigene Zuegangscode fescht.",
en: "We'll send you a 6-digit code by email in a moment. After that you'll set your own access code.",
fr: "Nous t'enverrons dans un instant un code à 6 chiffres par e-mail. Ensuite, tu définiras ton propre code d'accès.",
pt: "Enviaremos em instantes um código de 6 dígitos para o seu e-mail. Depois você define o seu próprio código de acesso.",
},
ab_feld_code: { ab_feld_code: {
de: "Code", "de-CH": "Code", de: "Code", "de-CH": "Code",
en: "Code", fr: "Code", pt: "Código", en: "Code", fr: "Code", pt: "Código",
@@ -151,8 +213,23 @@ window.I18N_PAGE = {
en: "Confirm", fr: "Confirmer", pt: "Confirmar", en: "Confirm", fr: "Confirmer", pt: "Confirmar",
}, },
ab_eingeloggt_als: { ab_eingeloggt_als: {
de: "Eingeloggt. Noch ein letzter Schritt:", "de-CH": "Yyglogget. No en letschte Schritt:", de: "Eingeloggt.", "de-CH": "Yyglogget.",
en: "Logged in. One last step:", fr: "Connecté. Une dernière étape :", pt: "Conectado. Um último passo:", en: "Logged in.", fr: "Connecté.", pt: "Conectado.",
},
ab_subscribe_locked_hinweis: {
de: "Dein Konto ist bereit — die Bezahlung startet, sobald das Abo offiziell live geht. Schau einfach bald wieder vorbei.",
"de-CH": "Dis Konto isch parat — d Zahlig startet, sobald s Abo offiziell live gaht. Schau eifach bald wieder verbi.",
en: "Your account is ready — payment opens as soon as the subscription officially launches. Just check back soon.",
fr: "Ton compte est prêt — le paiement s'ouvrira dès le lancement officiel de l'abonnement. Repasse bientôt.",
pt: "Sua conta está pronta — o pagamento abre assim que a assinatura for lançada oficialmente. Volte em breve.",
},
ab_weiter_startseite: {
de: "Zur Startseite →", "de-CH": "Zur Startsiite →",
en: "To the homepage →", fr: "Vers l'accueil →", pt: "Para a página inicial →",
},
ab_weiter_streamplan: {
de: "Zum Streamplan →", "de-CH": "Zum Streamplan →",
en: "To the stream schedule →", fr: "Vers le programme des lives →", pt: "Para a programação de lives →",
}, },
ab_oder_email: { ab_oder_email: {
de: "oder mit E-Mail", "de-CH": "oder mit E-Mail", de: "oder mit E-Mail", "de-CH": "oder mit E-Mail",
+4
View File
@@ -97,6 +97,10 @@ window.I18N_PAGE = {
de: "Direkt ansehen →", "de-CH": "Direkt aluege →", de: "Direkt ansehen →", "de-CH": "Direkt aluege →",
en: "View directly →", fr: "Voir directement →", pt: "Ver diretamente →", en: "View directly →", fr: "Voir directement →", pt: "Ver diretamente →",
}, },
idx_jahres_event_modal_teilen: {
de: "📤 Teilen", "de-CH": "📤 Teile",
en: "📤 Share", fr: "📤 Partager", pt: "📤 Compartilhar",
},
idx_char_eyebrow: { idx_char_eyebrow: {
de: "🩵 Die Gesichter der Welten", "de-CH": "🩵 D Gsichter vo de Wälte", de: "🩵 Die Gesichter der Welten", "de-CH": "🩵 D Gsichter vo de Wälte",
+4
View File
@@ -58,6 +58,10 @@ window.I18N_PAGE = {
de: "Mehr erfahren →", "de-CH": "Mehr erfahre →", de: "Mehr erfahren →", "de-CH": "Mehr erfahre →",
en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →", en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →",
}, },
sp_radar_teilen: {
de: "📤 Teilen", "de-CH": "📤 Teile",
en: "📤 Share", fr: "📤 Partager", pt: "📤 Compartilhar",
},
sp_card1_tag: { sp_card1_tag: {
de: "Fixer Termin", "de-CH": "Fixe Termin", de: "Fixer Termin", "de-CH": "Fixe Termin",
en: "Fixed Time", fr: "Rendez-vous fixe", pt: "Horário Fixo", en: "Fixed Time", fr: "Rendez-vous fixe", pt: "Horário Fixo",
+11
View File
@@ -58,6 +58,17 @@ window.I18N_PAGE = {
de: "Jetzt abonnieren", "de-CH": "Jetzt abonniere", de: "Jetzt abonnieren", "de-CH": "Jetzt abonniere",
en: "Subscribe now", fr: "S'abonner maintenant", pt: "Assine agora", en: "Subscribe now", fr: "S'abonner maintenant", pt: "Assine agora",
}, },
su_kein_abo_soon: {
de: "Coming soon…", "de-CH": "Coming soon…",
en: "Coming soon…", fr: "Coming soon…", pt: "Coming soon…",
},
su_kein_abo_hinweis: {
de: "Alles hier ist schon für dich vorbereitet — sobald das Abo startet, kannst du es mit einem Klick aktivieren.",
"de-CH": "Alles da isch scho für di parat — sobald s Abo startet, chasch es mit eim Klick aktiviere.",
en: "Everything here is already set up for you — as soon as the subscription starts, you can activate it with one click.",
fr: "Tout est déjà prêt pour toi — dès le lancement de l'abonnement, tu pourras l'activer en un clic.",
pt: "Tudo aqui já está preparado para você — assim que a assinatura começar, você poderá ativá-la com um clique.",
},
su_fenster2_h2: { su_fenster2_h2: {
de: "Mein aktueller Fortschritt", "de-CH": "Mis aktuelle Fortschritt", de: "Mein aktueller Fortschritt", "de-CH": "Mis aktuelle Fortschritt",
en: "My Current Progress", fr: "Ma progression actuelle", pt: "Meu progresso atual", en: "My Current Progress", fr: "Ma progression actuelle", pt: "Meu progresso atual",
+58 -7
View File
@@ -518,16 +518,24 @@
} }
const token = localStorage.getItem("supporterToken"); const token = localStorage.getItem("supporterToken");
const status = localStorage.getItem("supporterSubStatus");
if (!token) { if (!token) {
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span>${soonBadge()}</a>`; return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span>${soonBadge()}</a>`;
} }
const hatBereich = ["active", "pending", "suspended", "cancelled"].includes(status); /* Nutzer-Wunsch 21.08.2026: "meine seite ändert sich nicht und es gibt
if (hatBereich) { keinen button für registrierte sich normal anzumelden und abzumelden.
const name = localStorage.getItem("supporterDisplayName"); wo sie die speziellen sachen sehen als supporter und dass sie auch
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`; schon den abo sehen aber gesperrt ist." Vorher landeten Leute mit
} Konto, aber ohne bezahltes Abo (status "none"), im Nav-Knopf wieder
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_aktivieren")}</span>${soonBadge()}</a>`; nur bei abonnieren.html -- man merkte dem Knopf also nie an, dass man
überhaupt eingeloggt ist. supporter.html zeigt für genau diesen Fall
schon lange den Bereich "Mein Abo" -> #supp-keine-abo ("Du hast noch
kein aktives Abo" + Knopf) UND den Ausloggen-Knopf (siehe dort,
#btn-logout) -- das musste also nur noch verlinkt werden. Ab jetzt:
JEDER mit gültigem Token (egal ob "none", "active", "pending", ...)
geht auf supporter.html und sieht dort automatisch den passenden
Zustand samt Logout. */
const name = localStorage.getItem("supporterDisplayName");
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`;
} }
function renderHeader() { function renderHeader() {
@@ -1172,6 +1180,49 @@
return feld[sprache] || feld.de || ""; return feld[sprache] || feld.de || "";
}; };
/* =====================================================================
dogiTeilen() — 21.08.2026, Nutzer-Wunsch: "die events des jahres und
speziellen live events soll man auch teilen können." Wiederverwendbare
Teilen-Funktion für einzelne Inhalte (aktuell: Event des Jahres auf
index.html, spezielles Live-Event auf streamplan.html — künftig bei
Bedarf für weitere Stellen nutzbar, ohne den Mechanismus neu zu bauen).
Nutzt zuerst die native Teilen-Funktion des Geräts (Web-Share-API) —
auf dem Handy öffnet das den echten Teilen-Dialog (WhatsApp, TikTok,
Mail, ...). Die meisten Desktop-Browser kennen diese API nicht/zeigen
dort ohnehin kein sinnvolles Menü -- dort stattdessen Fallback: Link in
die Zwischenablage kopieren, der klickende Knopf bekommt kurz ein
"✓ Link kopiert"-Feedback (eigener Text + eigene Klasse, gleiches
Timing wie .vw-copy-btn in der Verwaltung). */
window.dogiTeilen = async function (daten, knopf) {
const payload = { title: daten.title || "", text: daten.text || "", url: daten.url || location.href };
if (navigator.share) {
try {
await navigator.share(payload);
return;
} catch (err) {
if (err && err.name === "AbortError") return; // Dialog selbst abgebrochen -- kein Fallback nötig
// sonst: weiter zum Zwischenablage-Fallback unten
}
}
if (!navigator.clipboard) return;
try {
await navigator.clipboard.writeText(payload.url);
if (knopf) {
const alterText = knopf.textContent;
knopf.textContent = "✓ Link kopiert";
knopf.classList.add("dogi-teilen-kopiert");
setTimeout(() => {
knopf.textContent = alterText;
knopf.classList.remove("dogi-teilen-kopiert");
}, 1600);
}
} catch {
/* Zwischenablage nicht erreichbar (z.B. kein HTTPS-Kontext) -- kein
Blocker, der Link bleibt über die normale Adressleiste erreichbar. */
}
};
/* ===================================================================== /* =====================================================================
dogiTeamLaden() — 21.08.2026, Nutzer-Wunsch: "überall wo Modis sind dogiTeamLaden() — 21.08.2026, Nutzer-Wunsch: "überall wo Modis sind
oder Scouts oder Manager, oder Creator, will ich dass ich die easy oder Scouts oder Manager, oder Creator, will ich dass ich die easy
+21
View File
@@ -156,7 +156,9 @@
const seCountdown = document.getElementById("se-radar-countdown"); const seCountdown = document.getElementById("se-radar-countdown");
const seTitel = document.getElementById("se-radar-titel"); const seTitel = document.getElementById("se-radar-titel");
const seLink = document.getElementById("se-radar-link"); const seLink = document.getElementById("se-radar-link");
const seTeilenBtn = document.getElementById("se-radar-teilen");
let zielDatum = null; let zielDatum = null;
let aktuellesEvent = null; // für dogiTeilen() beim Klick (21.08.2026)
let countdownIntervalId = null; let countdownIntervalId = null;
let zeigerIntervalId = null; let zeigerIntervalId = null;
@@ -201,6 +203,7 @@
return; return;
} }
zielDatum = new Date(event.zieldatum); zielDatum = new Date(event.zieldatum);
aktuellesEvent = event;
seWrap.hidden = false; seWrap.hidden = false;
seRadar.classList.remove("ist-live"); seRadar.classList.remove("ist-live");
seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || ""; seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || "";
@@ -226,5 +229,23 @@
document.addEventListener("dogi-sprache-geaendert", () => { document.addEventListener("dogi-sprache-geaendert", () => {
if (zielDatum) specialEventLaden(); if (zielDatum) specialEventLaden();
}); });
// Teilen (21.08.2026, Nutzer-Wunsch: "... speziellen live events soll man
// auch teilen können") -- teilt die Streamplan-Seite selbst (dort steht
// ja bereits automatisch das aktuell aktive Event), zusammen mit Titel
// im Text, damit klar ist, worum es geht. Nur EIN spezielles Event kann
// je aktiv sein, deshalb reicht ein einziger Knopf ohne Slot-Bezug.
seTeilenBtn?.addEventListener("click", () => {
if (!aktuellesEvent) return;
const titel = window.dogiFeld ? window.dogiFeld(aktuellesEvent.titel) : aktuellesEvent.titel?.de || "";
window.dogiTeilen?.(
{
title: titel,
text: `🔴 ${titel} — spezielles Live-Event bei DogFather!`,
url: `${location.origin}/streamplan.html`,
},
seTeilenBtn
);
});
} }
})(); })();
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Du bist Streamer und suchst einen Manager? Bewirb dich als Creator bei Spicy Media." /> <meta name="twitter:description" content="Du bist Streamer und suchst einen Manager? Bewirb dich als Creator bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-creator.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-creator-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-creator.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-creator-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -70,8 +70,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821n"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-creator.js?v=20260821n"></script> <script src="assets/js/i18n-bewerben-creator.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -22,10 +22,10 @@
<meta name="twitter:description" content="Kooperationsanfrage an Manager DogFather / Spicy Media." /> <meta name="twitter:description" content="Kooperationsanfrage an Manager DogFather / Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -68,8 +68,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821n"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-kooperation.js?v=20260821n"></script> <script src="assets/js/i18n-bewerben-kooperation.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Werde Modi bei Team Dogi — DogFathers persönliches Community-Team." /> <meta name="twitter:description" content="Werde Modi bei Team Dogi — DogFathers persönliches Community-Team." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-modi.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-modi-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-dogfather.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-bewerben-modi.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="bwm_eyebrow">🩵 Team Dogi — Community</span> <span class="eyebrow" data-i18n="bwm_eyebrow">🩵 Team Dogi — Community</span>
<h1 data-i18n="bwm_h1">Werde Modi bei Team Dogi</h1> <h1 data-i18n="bwm_h1">Werde Modi bei Team Dogi</h1>
@@ -79,8 +79,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821n"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-modi.js?v=20260821n"></script> <script src="assets/js/i18n-bewerben-modi.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Werde Scout für DogFather bei der Spicy Media Agentur — finde neue Talente." /> <meta name="twitter:description" content="Werde Scout für DogFather bei der Spicy Media Agentur — finde neue Talente." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-scout.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-scout-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-scouts.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-bewerben-scout.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="bws_eyebrow">Manager DogFather — Agentur</span> <span class="eyebrow" data-i18n="bws_eyebrow">Manager DogFather — Agentur</span>
<h1 data-i18n="bws_h1">Werde Scout für DogFather</h1> <h1 data-i18n="bws_h1">Werde Scout für DogFather</h1>
@@ -71,8 +71,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821n"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-scout.js?v=20260821n"></script> <script src="assets/js/i18n-bewerben-scout.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Werde Teil vom Team: Modi bei Team Dogi, Scout oder Creator bei Spicy Media, oder Kooperation anfragen." /> <meta name="twitter:description" content="Werde Teil vom Team: Modi bei Team Dogi, Scout oder Creator bei Spicy Media, oder Kooperation anfragen." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-split.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-split-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-split.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-split-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -85,7 +85,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-bewerben.js?v=20260821n"></script> <script src="assets/js/i18n-bewerben.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Der Husky an DogFathers Seite — das tierische Gesicht der HasiDog-Welt." /> <meta name="twitter:description" content="Der Husky an DogFathers Seite — das tierische Gesicht der HasiDog-Welt." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -85,7 +85,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-casper.js?v=20260821n"></script> <script src="assets/js/i18n-casper.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
@@ -0,0 +1,27 @@
-- =====================================================================
-- 0009_supporter_access_code.sql — Eigener, fester Zugangscode je Supporter
--
-- Nutzer-Wunsch 21.08.2026: "wieso sollte ich wieder ein code bekommen,
-- mach die anmeldung einfach, die leute sollen nach der registrierung einen
-- festen code erstellen können für sich selbst und fertig. mit dem sie rein
-- kommen."
--
-- Bis hierher lief JEDER Login über einen 6-stelligen Einmalcode per E-Mail
-- (supporter_auth_codes) — auch für längst registrierte Leute. Ab jetzt legt
-- man bei der Registrierung einen eigenen, dauerhaften Zugangscode fest und
-- kommt damit direkt rein.
--
-- Gespeichert wird NIE der Code selbst, sondern ein scrypt-Hash inkl.
-- zufälligem Salt (Format "scrypt$<salt-hex>$<hash-hex>", siehe
-- lib/crypto.js). Damit ist der Code selbst aus der Datenbank heraus nicht
-- rekonstruierbar — auch nicht von uns.
--
-- Der E-Mail-Einmalcode-Weg bleibt vollständig erhalten: er ist jetzt der
-- "Code vergessen?"-Weg, über den man sich einen neuen Zugangscode setzen
-- kann. Deshalb wird an supporter_auth_codes nichts geändert.
--
-- NULL = Konto aus der Zeit vor dieser Änderung, hat noch keinen eigenen
-- Code. Solche Konten kommen über "Code vergessen?" an einen.
-- =====================================================================
ALTER TABLE supporter_users ADD COLUMN access_code_hash TEXT;
ALTER TABLE supporter_users ADD COLUMN access_code_set_at TEXT;
@@ -0,0 +1,23 @@
-- =====================================================================
-- 0010_testimonials_avatar.sql — Profilbild für die "Stimmen"
--
-- Nutzer-Wunsch 21.08.2026: "ich soll auch paar geile bilder zum aussuchen
-- haben als profilbild falls die leute kein bild mit schicken, das soll
-- denen auch als möglichkeit gegeben sein."
--
-- Bis hierher zeigte jede Stimme nur den Anfangsbuchstaben des Namens in
-- einem Kreis. Jetzt kann ein Bild hinterlegt werden.
--
-- Inhalt der Spalte:
-- * eine Auswahlbild-Id ("husky-hoodie", "casper", …) — siehe
-- ERLAUBTE_AVATARE in server-internal/routes/testimonials.js und
-- assets/js/data-stimmen-avatare.js
-- * ODER eine vollständige Adresse eines von der Verwaltung
-- hochgeladenen eigenen Fotos
-- * ODER NULL = kein Bild, dann bleibt es beim Anfangsbuchstaben
--
-- Bewusst nur EINE Spalte statt zweier (Auswahlbild/eigenes Foto): es kann
-- ohnehin immer nur eines von beiden gleichzeitig gelten, zwei Spalten
-- hätten nur die Frage aufgeworfen, welche gewinnt.
-- =====================================================================
ALTER TABLE testimonials ADD COLUMN avatar TEXT;
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Team Dogi ist Familie — die Community rund um DogFather und HasiDog." /> <meta name="twitter:description" content="Team Dogi ist Familie — die Community rund um DogFather und HasiDog." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-community.jpg');--page-bg-mobile:url('/assets/img/bg-community-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-community.jpg');--page-bg-mobile:url('/assets/img/bg-community-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -84,7 +84,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-community.js?v=20260821n"></script> <script src="assets/js/i18n-community.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Content Creator, die Teil von DogFather / Spicy Media sind." /> <meta name="twitter:description" content="Content Creator, die Teil von DogFather / Spicy Media sind." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-creator.jpg');--page-bg-mobile:url('/assets/img/bg-creator-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-creator.jpg');--page-bg-mobile:url('/assets/img/bg-creator-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -60,9 +60,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-creator.js?v=20260821n"></script> <script src="assets/js/data-creator.js?v=20260821o"></script>
<script src="assets/js/i18n-creator.js?v=20260821n"></script> <script src="assets/js/i18n-creator.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
const grid = document.getElementById("creator-grid"); const grid = document.getElementById("creator-grid");
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Wer ist DogFather wirklich? Persönlichkeit, Werte und die Geschichte hinter dem Content." /> <meta name="twitter:description" content="Wer ist DogFather wirklich? Persönlichkeit, Werte und die Geschichte hinter dem Content." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather-person.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-person-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather-person.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-person-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -158,7 +158,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-dogfather.js?v=20260821n"></script> <script src="assets/js/i18n-dogfather.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -26,8 +26,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="DogFather" /> <meta name="apple-mobile-web-app-title" content="DogFather" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<style> <style>
/* ===================================================================== /* =====================================================================
Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="HasiDog: Maskottchen, Musikprojekt und die verspielte, familiäre Seite von Team Dogi." /> <meta name="twitter:description" content="HasiDog: Maskottchen, Musikprojekt und die verspielte, familiäre Seite von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -186,7 +186,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-hasidog.js?v=20260821n"></script> <script src="assets/js/i18n-hasidog.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+8 -8
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="DOGFATHER UNIVERSE: die Heimat von DogFather als Streamer, der HasiDog-Community und Manager DogFather bei Spicy Media." /> <meta name="twitter:description" content="DOGFATHER UNIVERSE: die Heimat von DogFather als Streamer, der HasiDog-Community und Manager DogFather bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="start-seite"> <body class="start-seite">
<div id="site-header"></div> <div id="site-header"></div>
@@ -290,10 +290,10 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821n"></script> <script src="assets/js/data-modis.js?v=20260821o"></script>
<script src="assets/js/data-scouts.js?v=20260821n"></script> <script src="assets/js/data-scouts.js?v=20260821o"></script>
<script src="assets/js/i18n-index.js?v=20260821n"></script> <script src="assets/js/i18n-index.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
// Gleiche URL wie in assets/js/forms.js / postfach.html — läuft seit // Gleiche URL wie in assets/js/forms.js / postfach.html — läuft seit
// 05.08.2026 auf dem eigenen Server statt auf Cloudflare Workers. // 05.08.2026 auf dem eigenen Server statt auf Cloudflare Workers.
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Kontakt, Impressum und Datenschutz von DOGFATHER UNIVERSE." /> <meta name="twitter:description" content="Kontakt, Impressum und Datenschutz von DOGFATHER UNIVERSE." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -126,7 +126,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-kontakt.js?v=20260821n"></script> <script src="assets/js/i18n-kontakt.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+8 -8
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Alle Social-Media- und Musik-Links von DogFather an einem Ort — plus QR-Codes." /> <meta name="twitter:description" content="Alle Social-Media- und Musik-Links von DogFather an einem Ort — plus QR-Codes." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* Rabattcodes: an das Supporter-Abo gekoppelt (Nutzer-Feedback /* Rabattcodes: an das Supporter-Abo gekoppelt (Nutzer-Feedback
03.08.2026: "verbinde es mit dem Abo... wenn die Leute drauf drücken 03.08.2026: "verbinde es mit dem Abo... wenn die Leute drauf drücken
@@ -46,12 +46,12 @@
.rabatt-gesperrt-hinweis p { color: #ffe9a8; margin: 0; flex: 1 1 260px; font-size: .95rem; } .rabatt-gesperrt-hinweis p { color: #ffe9a8; margin: 0; flex: 1 1 260px; font-size: .95rem; }
</style> </style>
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-links.jpg');--page-bg-mobile:url('/assets/img/bg-links-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-links-seite.jpg');--page-bg-mobile:url('/assets/img/bg-links-seite-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-links.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-links-seite.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="lk_eyebrow">🩵 Alle Links</span> <span class="eyebrow" data-i18n="lk_eyebrow">🩵 Alle Links</span>
<h1 data-i18n="lk_h1">Alles an einem Ort</h1> <h1 data-i18n="lk_h1">Alles an einem Ort</h1>
@@ -140,8 +140,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-links.js?v=20260821n"></script> <script src="assets/js/i18n-links.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="DogFather als Manager bei der Spicy Media Agency: Scouts, Creator und faire Zusammenarbeit." /> <meta name="twitter:description" content="DogFather als Manager bei der Spicy Media Agency: Scouts, Creator und faire Zusammenarbeit." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-manager.jpg');--page-bg-mobile:url('/assets/img/bg-manager-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-manager.jpg');--page-bg-mobile:url('/assets/img/bg-manager-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -139,9 +139,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-manager.js?v=20260821n"></script> <script src="assets/js/data-manager.js?v=20260821o"></script>
<script src="assets/js/i18n-manager.js?v=20260821n"></script> <script src="assets/js/i18n-manager.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
// "Weitere Manager" (21.08.2026) -- siehe Kommentar bei #weitere-manager-root // "Weitere Manager" (21.08.2026) -- siehe Kommentar bei #weitere-manager-root
// oben. Aktuell IMMER window.MANAGERS === [] (siehe data-manager.js), Inhalt // oben. Aktuell IMMER window.MANAGERS === [] (siehe data-manager.js), Inhalt
+11 -11
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Fotos und Videos rund um Husky Casper an einem Ort." /> <meta name="twitter:description" content="Fotos und Videos rund um Husky Casper an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" data-welt="casper" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');"> <body class="mit-hintergrund" data-welt="casper" style="--page-bg:url('/assets/img/bg-medien-casper.jpg');--page-bg-mobile:url('/assets/img/bg-medien-casper-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-casper.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-medien-casper.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="mc_eyebrow">Welt 2 — HasiDog</span> <span class="eyebrow" data-i18n="mc_eyebrow">Welt 2 — HasiDog</span>
<h1 data-i18n="mc_h1">Medien — Casper</h1> <h1 data-i18n="mc_h1">Medien — Casper</h1>
@@ -59,10 +59,10 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js?v=20260821n"></script> <script src="assets/js/data-media.js?v=20260821o"></script>
<script src="assets/js/data-videos.js?v=20260821n"></script> <script src="assets/js/data-videos.js?v=20260821o"></script>
<script src="assets/js/medien-welt.js?v=20260821n"></script> <script src="assets/js/medien-welt.js?v=20260821o"></script>
<script src="assets/js/i18n-medien-casper.js?v=20260821n"></script> <script src="assets/js/i18n-medien-casper.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Fotos, Videos und Events aus DogFathers Welt an einem Ort." /> <meta name="twitter:description" content="Fotos, Videos und Events aus DogFathers Welt an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" data-welt="dogfather" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');"> <body class="mit-hintergrund" data-welt="dogfather" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -65,11 +65,11 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js?v=20260821n"></script> <script src="assets/js/data-media.js?v=20260821o"></script>
<script src="assets/js/data-videos.js?v=20260821n"></script> <script src="assets/js/data-videos.js?v=20260821o"></script>
<script src="assets/js/data-aktionen.js?v=20260821n"></script> <script src="assets/js/data-aktionen.js?v=20260821o"></script>
<script src="assets/js/medien-welt.js?v=20260821n"></script> <script src="assets/js/medien-welt.js?v=20260821o"></script>
<script src="assets/js/i18n-medien-dogfather.js?v=20260821n"></script> <script src="assets/js/i18n-medien-dogfather.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Fotos, Videos und Events aus der HasiDog-Welt an einem Ort." /> <meta name="twitter:description" content="Fotos, Videos und Events aus der HasiDog-Welt an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" data-welt="hasidog" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');"> <body class="mit-hintergrund" data-welt="hasidog" style="--page-bg:url('/assets/img/bg-medien-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-medien-hasidog-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-hasidog.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-medien-hasidog.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="mh_eyebrow">Welt 2 — HasiDog</span> <span class="eyebrow" data-i18n="mh_eyebrow">Welt 2 — HasiDog</span>
<h1 data-i18n="mh_h1">Medien — HasiDog</h1> <h1 data-i18n="mh_h1">Medien — HasiDog</h1>
@@ -65,11 +65,11 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js?v=20260821n"></script> <script src="assets/js/data-media.js?v=20260821o"></script>
<script src="assets/js/data-videos.js?v=20260821n"></script> <script src="assets/js/data-videos.js?v=20260821o"></script>
<script src="assets/js/data-aktionen.js?v=20260821n"></script> <script src="assets/js/data-aktionen.js?v=20260821o"></script>
<script src="assets/js/medien-welt.js?v=20260821n"></script> <script src="assets/js/medien-welt.js?v=20260821o"></script>
<script src="assets/js/i18n-medien-hasidog.js?v=20260821n"></script> <script src="assets/js/i18n-medien-hasidog.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -16,10 +16,10 @@
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="robots" content="noindex" /> <!-- generische URL, echte Team-Seiten verlinken hierher --> <meta name="robots" content="noindex" /> <!-- generische URL, echte Team-Seiten verlinken hierher -->
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
.profil-photo { aspect-ratio: 4/5; max-width: 420px; margin: 0 auto; } .profil-photo { aspect-ratio: 4/5; max-width: 420px; margin: 0 auto; }
.profil-photo img { width: 100%; height: 100%; object-fit: cover; display: block; } .profil-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
@@ -130,12 +130,12 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821n"></script> <script src="assets/js/data-modis.js?v=20260821o"></script>
<script src="assets/js/data-scouts.js?v=20260821n"></script> <script src="assets/js/data-scouts.js?v=20260821o"></script>
<script src="assets/js/data-creator.js?v=20260821n"></script> <script src="assets/js/data-creator.js?v=20260821o"></script>
<script src="assets/js/data-manager.js?v=20260821n"></script> <script src="assets/js/data-manager.js?v=20260821o"></script>
<script src="assets/js/i18n-profil.js?v=20260821n"></script> <script src="assets/js/i18n-profil.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
const slug = new URLSearchParams(location.search).get("p") || ""; const slug = new URLSearchParams(location.search).get("p") || "";
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Rabattcodes von Dogis Partnern für Team Dogi — spar bares Geld bei ausgewählten Marken." /> <meta name="twitter:description" content="Rabattcodes von Dogis Partnern für Team Dogi — spar bares Geld bei ausgewählten Marken." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* Leere-Zustand-Kachel im "Gutschein"-Look: gestrichelter Rand wie ein /* Leere-Zustand-Kachel im "Gutschein"-Look: gestrichelter Rand wie ein
ausgeschnittener Coupon, damit auch OHNE echte Codes schon klar ist, ausgeschnittener Coupon, damit auch OHNE echte Codes schon klar ist,
@@ -131,8 +131,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-rabattcodes.js?v=20260821n"></script> <script src="assets/js/i18n-rabattcodes.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
+2
View File
@@ -32,6 +32,8 @@ const MIGRATION_FILES = [
"0006_testimonials.sql", "0006_testimonials.sql",
"0007_team_members.sql", "0007_team_members.sql",
"0008_team_members_rich.sql", "0008_team_members_rich.sql",
"0009_supporter_access_code.sql",
"0010_testimonials_avatar.sql",
]; ];
function alreadyApplied(name) { function alreadyApplied(name) {
+25
View File
@@ -53,6 +53,7 @@ import {
import { listAuditLog } from "./routes/audit.js"; import { listAuditLog } from "./routes/audit.js";
import { import {
registerSupporter, verifySupporterEmail, requestSupporterLogin, verifySupporterLogin, logoutSupporter, registerSupporter, verifySupporterEmail, requestSupporterLogin, verifySupporterLogin, logoutSupporter,
loginSupporterMitCode, setSupporterAccessCode,
googleLoginSupporter, getPublicConfig, googleLoginSupporter, getPublicConfig,
getSupporterMe, updateSupporterProfile, requestSupporterEmailChange, confirmSupporterEmailChange, getSupporterMe, updateSupporterProfile, requestSupporterEmailChange, confirmSupporterEmailChange,
redeemPremium, deleteSupporterAccount, redeemPremium, deleteSupporterAccount,
@@ -67,6 +68,7 @@ import { getEventsPublic, getEventsAdmin, saveEvent, uploadEventImage } from "./
import { getSpecialEventPublic, getSpecialEventAdmin, saveSpecialEvent } from "./routes/special-event.js"; import { getSpecialEventPublic, getSpecialEventAdmin, saveSpecialEvent } from "./routes/special-event.js";
import { import {
submitTestimonial, getApprovedTestimonialsPublic, getTestimonialsAdmin, setTestimonialStatus, deleteTestimonial, submitTestimonial, getApprovedTestimonialsPublic, getTestimonialsAdmin, setTestimonialStatus, deleteTestimonial,
setTestimonialAvatar, uploadTestimonialAvatar,
} from "./routes/testimonials.js"; } from "./routes/testimonials.js";
import { import {
createPoll, listPolls, closePoll, reopenPoll, deletePoll, getActivePollPublic, voteOnPoll, createPoll, listPolls, closePoll, reopenPoll, deletePoll, getActivePollPublic, voteOnPoll,
@@ -116,6 +118,20 @@ const teamBildUpload = multer({
fileFilter: (req, file, cb) => cb(null, ERLAUBTE_BILD_TYPEN.has(file.mimetype)), fileFilter: (req, file, cb) => cb(null, ERLAUBTE_BILD_TYPEN.has(file.mimetype)),
}); });
// Profilbilder für die "Stimmen" (21.08.2026) — gleiches Muster wie oben.
// Nur aus der Verwaltung heraus erreichbar; das öffentliche Stimmen-Formular
// nimmt bewusst keine Dateien entgegen (siehe routes/testimonials.js).
const TESTIMONIAL_UPLOADS_DIR = `${UPLOADS_BASIS_DIR}/testimonials`;
mkdirSync(TESTIMONIAL_UPLOADS_DIR, { recursive: true });
const stimmenBildUpload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, TESTIMONIAL_UPLOADS_DIR),
filename: (req, file, cb) => cb(null, `${crypto.randomUUID()}${extname(file.originalname).toLowerCase()}`),
}),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter: (req, file, cb) => cb(null, ERLAUBTE_BILD_TYPEN.has(file.mimetype)),
});
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
@@ -185,6 +201,9 @@ app.get("/site-texts", wrap(getSiteTextsPublic));
app.post("/supporter/register", wrap(registerSupporter)); app.post("/supporter/register", wrap(registerSupporter));
app.post("/supporter/verify-email", wrap(verifySupporterEmail)); app.post("/supporter/verify-email", wrap(verifySupporterEmail));
// Normaler Login seit 21.08.2026: E-Mail + selbst gewählter Zugangscode.
app.post("/supporter/login-code", wrap(loginSupporterMitCode));
// login-request/verify-login sind jetzt der "Code vergessen?"-Weg.
app.post("/supporter/login-request", wrap(requestSupporterLogin)); app.post("/supporter/login-request", wrap(requestSupporterLogin));
app.post("/supporter/verify-login", wrap(verifySupporterLogin)); app.post("/supporter/verify-login", wrap(verifySupporterLogin));
app.post("/supporter/google-login", wrap(googleLoginSupporter)); app.post("/supporter/google-login", wrap(googleLoginSupporter));
@@ -227,6 +246,9 @@ app.post("/admin/special-event/save", requireTeamSession, withSession(saveSpecia
app.post("/admin/testimonials/list", requireTeamSession, withSession(getTestimonialsAdmin)); app.post("/admin/testimonials/list", requireTeamSession, withSession(getTestimonialsAdmin));
app.post("/admin/testimonials/status", requireTeamSession, withSession(setTestimonialStatus)); app.post("/admin/testimonials/status", requireTeamSession, withSession(setTestimonialStatus));
app.post("/admin/testimonials/delete", requireTeamSession, withSession(deleteTestimonial)); app.post("/admin/testimonials/delete", requireTeamSession, withSession(deleteTestimonial));
// Profilbild einer Stimme setzen bzw. eigenes Foto hochladen (21.08.2026).
app.post("/admin/testimonials/avatar", requireTeamSession, withSession(setTestimonialAvatar));
app.post("/admin/testimonials/upload-avatar", requireTeamSession, stimmenBildUpload.single("image"), withSession(uploadTestimonialAvatar));
// Team-Mitglieder (Modis/Scouts/Creator/Manager) — 21.08.2026 // Team-Mitglieder (Modis/Scouts/Creator/Manager) — 21.08.2026
app.post("/admin/team/list", requireTeamSession, withSession(getTeamAdmin)); app.post("/admin/team/list", requireTeamSession, withSession(getTeamAdmin));
@@ -277,6 +299,9 @@ app.post("/admin/polls/delete", requireTeamSession, withSession(deletePoll));
app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter)); app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter));
app.post("/supporter/me", requireSupporterAuth, withSupporter(getSupporterMe)); app.post("/supporter/me", requireSupporterAuth, withSupporter(getSupporterMe));
app.post("/supporter/update-profile", requireSupporterAuth, withSupporter(updateSupporterProfile)); app.post("/supporter/update-profile", requireSupporterAuth, withSupporter(updateSupporterProfile));
// Neuen Zugangscode setzen — freiwillig im Supporter-Bereich oder als
// Abschluss des "Code vergessen?"-Weges (da ist man bereits eingeloggt).
app.post("/supporter/set-access-code", requireSupporterAuth, withSupporter(setSupporterAccessCode));
app.post("/supporter/request-email-change", requireSupporterAuth, withSupporter(requestSupporterEmailChange)); app.post("/supporter/request-email-change", requireSupporterAuth, withSupporter(requestSupporterEmailChange));
app.post("/supporter/confirm-email-change", requireSupporterAuth, withSupporter(confirmSupporterEmailChange)); app.post("/supporter/confirm-email-change", requireSupporterAuth, withSupporter(confirmSupporterEmailChange));
app.post("/supporter/redeem-premium", requireSupporterAuth, withSupporter(redeemPremium)); app.post("/supporter/redeem-premium", requireSupporterAuth, withSupporter(redeemPremium));
+54
View File
@@ -60,3 +60,57 @@ export async function decryptCode(encoded) {
export function nowIso() { export function nowIso() {
return new Date().toISOString(); return new Date().toISOString();
} }
/* =====================================================================
Persönlicher Zugangscode der Supporter (21.08.2026)
BEWUSST NICHT sha256Hex() wie bei den Einmalcodes oben: ein Einmalcode
lebt 15 Minuten und ist zufällig, da genügt ein schneller Hash. Ein
selbst gewählter, dauerhafter Code ist dagegen oft kurz und erratbar —
dagegen hilft nur ein absichtlich LANGSAMES Verfahren, das massenhaftes
Durchprobieren unbezahlbar macht. Dafür ist scrypt gemacht (in Node
eingebaut, keine zusätzliche Abhängigkeit nötig).
Jeder Code bekommt zusätzlich einen eigenen Zufalls-Salt: dadurch haben
zwei Personen mit demselben Code trotzdem völlig verschiedene Hashes,
und vorberechnete Tabellen sind wertlos.
Gespeichertes Format: "scrypt$<salt-hex>$<hash-hex>"
===================================================================== */
import { scrypt as _scrypt, randomBytes, timingSafeEqual as _timingSafeEqual } from "node:crypto";
const SCRYPT_KEYLEN = 64;
// N=16384 ist der Node-Standard und ein guter Kompromiss: für eine einzelne
// Anmeldung kaum spürbar, für Angreifer aber millionenfach teuer.
const SCRYPT_OPTIONEN = { N: 16384, r: 8, p: 1 };
function scryptAsync(code, salt) {
return new Promise((resolve, reject) => {
_scrypt(code, salt, SCRYPT_KEYLEN, SCRYPT_OPTIONEN, (err, key) => (err ? reject(err) : resolve(key)));
});
}
/** Erzeugt den zu speichernden Wert für einen selbst gewählten Zugangscode. */
export async function hashAccessCode(code) {
const salt = randomBytes(16);
const key = await scryptAsync(String(code), salt);
return `scrypt$${salt.toString("hex")}$${key.toString("hex")}`;
}
/** Prüft einen eingegebenen Code gegen den gespeicherten Wert.
Der Vergleich läuft zeitkonstant (timingSafeEqual), damit sich aus der
Antwortdauer nicht ableiten lässt, wie viele Zeichen gestimmt haben. */
export async function verifyAccessCode(code, gespeichert) {
if (!gespeichert || typeof gespeichert !== "string") return false;
const teile = gespeichert.split("$");
if (teile.length !== 3 || teile[0] !== "scrypt") return false;
try {
const salt = Buffer.from(teile[1], "hex");
const erwartet = Buffer.from(teile[2], "hex");
const tatsaechlich = await scryptAsync(String(code), salt);
if (erwartet.length !== tatsaechlich.length) return false;
return _timingSafeEqual(erwartet, tatsaechlich);
} catch {
return false;
}
}
+21
View File
@@ -60,6 +60,27 @@ export async function sendLoginCode({ to, name, code }) {
}); });
} }
/* Willkommens-E-Mail nach der Registrierung (21.08.2026). Enthält bewusst
KEINEN Code — man hat sich ja gerade selbst einen gesetzt und ist schon
eingeloggt. Sie dient als Beleg, dass die Adresse stimmt, und als
Erinnerung, wo man wieder reinkommt. */
export async function sendWelcomeMail({ to, name }) {
return send({
to,
subject: "Willkommen in der DogiCrew 🩵",
html: WRAP(
`Willkommen, ${name}! 🩵`,
`<p style="line-height:1.6;">Dein Konto ist fertig. Du kommst ab jetzt jederzeit mit dieser
E-Mail-Adresse und deinem selbst gewählten Zugangscode rein.</p>
<p style="line-height:1.6;">Solltest du deinen Code mal vergessen, gibt es auf der Anmeldeseite
„Code vergessen?“ — darüber bekommst du hier per E-Mail einen Einmalcode und kannst dir
einen neuen Zugangscode setzen.</p>
<p style="color:#9aa4b2;font-size:13px;">Hast du dich nicht angemeldet? Dann ignoriere diese
E-Mail einfach — ohne deinen Zugangscode kommt niemand in das Konto.</p>`
),
});
}
export async function sendMilestoneUnlocked({ to, name, premiumLabel }) { export async function sendMilestoneUnlocked({ to, name, premiumLabel }) {
return send({ return send({
to, to,
+13 -2
View File
@@ -61,11 +61,22 @@ export async function getEventsAdmin(req, res, session) {
return json(res, { ok: true, events: [ladeSlot(1), ladeSlot(2)] }); return json(res, { ok: true, events: [ladeSlot(1), ladeSlot(2)] });
} }
/** NUR Bild-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware). */ /** NUR Bild-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware).
Bugfix 21.08.2026 (Nutzer-Report: "wieso seh ich das bild nicht" bei
"Event 1" in der Verwaltung): vorher wurde hier nur der RELATIVE Pfad
("/uploads/events/...") zurückgegeben. verwaltung.html UND index.html
laufen aber auf dogfather-universe.com (dogiweb-Dienst), während die
hochgeladene Datei tatsächlich unter postfach.dogfather-universe.com
(dieser Dienst, dogiintern, siehe /uploads-Static-Route in index.js)
liegt -- der Browser hat den relativen Pfad also fälschlich gegen die
falsche Domain aufgelöst und ein kaputtes Bild-Symbol angezeigt, sowohl
in der Verwaltungs-Vorschau als auch (sobald ein Slot aktiviert wird)
auf der echten Startseite. Jetzt vollständige Adresse, damit das Bild
unabhängig davon lädt, von welcher Domain aus die Seite gerade läuft. */
export async function uploadEventImage(req, res, session) { export async function uploadEventImage(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res); if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400); if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400);
const url = `/uploads/events/${req.file.filename}`; const url = `https://postfach.dogfather-universe.com/uploads/events/${req.file.filename}`;
logAction(session.actor, "events.image_uploaded", req.file.filename, null); logAction(session.actor, "events.image_uploaded", req.file.filename, null);
return json(res, { ok: true, url }); return json(res, { ok: true, url });
} }
+162 -11
View File
@@ -7,14 +7,15 @@
Setzt Dogfather_VanVan_Supporter_Abo.odt Abschnitt 5, 6, 9-18, 21-25 um. Setzt Dogfather_VanVan_Supporter_Abo.odt Abschnitt 5, 6, 9-18, 21-25 um.
===================================================================== */ ===================================================================== */
import { db } from "../db.js"; import { db } from "../db.js";
import { generateId, nowIso } from "../lib/crypto.js"; import { generateId, nowIso, hashAccessCode, verifyAccessCode } from "../lib/crypto.js";
import { json } from "../lib/http.js"; import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js"; import { logAction } from "../lib/audit.js";
import { isLockedOut, recordFailedAttempt, clearAttempts } from "../lib/auth.js";
import { import {
normalizeEmail, normalizeTikTokUsername, issueAuthCode, verifyAuthCode, normalizeEmail, normalizeTikTokUsername, issueAuthCode, verifyAuthCode,
createSupporterSession, endSupporterSession, createSupporterSession, endSupporterSession,
} from "../lib/supporter-auth.js"; } from "../lib/supporter-auth.js";
import { EmailNotConfiguredError, sendVerifyEmailCode, sendLoginCode } from "../lib/supporter-mail.js"; import { EmailNotConfiguredError, sendVerifyEmailCode, sendLoginCode, sendWelcomeMail } from "../lib/supporter-mail.js";
import { computeProgress, timelineForCurrentCycle } from "../lib/supporter-cycle.js"; import { computeProgress, timelineForCurrentCycle } from "../lib/supporter-cycle.js";
import { verifyGoogleIdToken, GoogleAuthNotConfiguredError } from "../lib/google-auth.js"; import { verifyGoogleIdToken, GoogleAuthNotConfiguredError } from "../lib/google-auth.js";
@@ -41,6 +42,52 @@ function emailErrorResponse(res, err) {
/* ---------- Registrierung & Login (öffentlich, keine Session) ---------- */ /* ---------- Registrierung & Login (öffentlich, keine Session) ---------- */
/* Mindestlänge für den selbst gewählten Zugangscode. Bewusst nur 6 Zeichen
(statt der sonst üblichen 8+): der Bereich enthält keine Zahlungsdaten,
und ein zu strenger Zwang würde hier mehr Leute abschrecken als schützen.
Gegen Durchprobieren schützt die Sperre nach mehreren Fehlversuchen
(siehe loginSupporterMitCode) plus das langsame scrypt-Verfahren. */
const MIN_CODE_LAENGE = 6;
function pruefeCode(code) {
const sauber = String(code || "");
if (sauber.length < MIN_CODE_LAENGE) {
return `Dein Zugangscode muss mindestens ${MIN_CODE_LAENGE} Zeichen lang sein.`;
}
if (sauber.length > 200) return "Dein Zugangscode ist zu lang.";
return null;
}
/* Registrierung — seit 21.08.2026 (Nutzer-Wunsch: "die leute sollen nach der
registrierung einen festen code erstellen können für sich selbst und
fertig, mit dem sie rein kommen"): man legt hier direkt seinen eigenen,
dauerhaften Zugangscode fest und ist danach SOFORT eingeloggt. Vorher lief
auch die Registrierung über einen Einmalcode per E-Mail — ein Extraschritt,
der nichts brachte, weil man denselben Code danach nie wieder brauchte.
Die E-Mail-Adresse gilt dabei als bestätigt (email_verified = 1). Ohne das
käme man später nicht mehr an den "Code vergessen?"-Weg heran, der ja
genau über diese Adresse läuft — und ein zusätzlicher Bestätigungsschritt
wäre wieder genau der Umweg, der hier abgeschafft werden sollte. Zur
Kontrolle geht eine Willkommens-E-Mail raus; schlägt deren Versand fehl,
ist das KEIN Grund die Registrierung scheitern zu lassen (das Konto ist
ja schon nutzbar) — sie wird nur im Protokoll vermerkt. */
/* Schritt 1 von 3 der Registrierung (Nutzer-Wunsch 21.08.2026: "bei der
ersten registrierung sollen die eine email bekommen mit einem einmaligen
code damit wir auch wissen dass email stimmt und danach wenn sie den code
eingegeben haben sollen die erst ihren zugangscode auswählen/eintippen
können").
Vorher wurde hier direkt `email_verified = 1` gesetzt UND der Zugangscode
im selben Schritt entgegengenommen -- die E-Mail-Adresse wurde also nie
geprüft. Man konnte sich mit einer fremden oder erfundenen Adresse
registrieren und kam sofort rein.
Jetzt legt dieser Schritt nur das UNBESTÄTIGTE Konto an (email_verified = 0,
noch KEIN Zugangscode) und verschickt einen Einmalcode. Weiter geht es über
verifySupporterEmail() (Schritt 2) und erst danach setSupporterAccessCode()
(Schritt 3). Es wird hier bewusst KEIN Session-Token zurückgegeben -- ohne
bestätigte E-Mail ist man nicht eingeloggt. */
export async function registerSupporter(req, res) { export async function registerSupporter(req, res) {
const body = req.body || {}; const body = req.body || {};
const displayName = String(body.displayName || "").trim().slice(0, 60); const displayName = String(body.displayName || "").trim().slice(0, 60);
@@ -50,31 +97,117 @@ export async function registerSupporter(req, res) {
if (!displayName) return bad(res, "Bitte gib einen Namen oder Anzeigenamen an."); if (!displayName) return bad(res, "Bitte gib einen Namen oder Anzeigenamen an.");
if (!email || !email.includes("@")) return bad(res, "Bitte gib eine gültige E-Mail-Adresse an."); if (!email || !email.includes("@")) return bad(res, "Bitte gib eine gültige E-Mail-Adresse an.");
const existing = db.prepare(`SELECT id, email_verified FROM supporter_users WHERE email = ?`).get(email); const existing = db.prepare(`SELECT id, access_code_hash FROM supporter_users WHERE email = ?`).get(email);
if (existing && existing.access_code_hash) {
return bad(res, "Für diese E-Mail-Adresse gibt es schon ein Konto. Bitte logg dich einfach ein.", 409);
}
let supporterId; let supporterId;
if (existing) { if (existing) {
if (existing.email_verified) { // Konto existiert, ist aber noch unfertig (abgebrochener Versuch, oder
return bad(res, "Für diese E-Mail-Adresse existiert bereits ein Supporter-Konto. Bitte einloggen.", 409); // Altkonto ohne eigenen Code) -> Angaben auffrischen, Bestätigung neu
} // anstoßen. Kein Fehler nach außen: ein zweiter Anlauf soll einfach
// funktionieren, statt in einer Sackgasse zu enden.
supporterId = existing.id; supporterId = existing.id;
db.prepare(`UPDATE supporter_users SET display_name = ?, tiktok_username = ? WHERE id = ?`).run(displayName, tiktokUsername, supporterId); db.prepare(
`UPDATE supporter_users
SET display_name = ?, tiktok_username = ?, email_verified = 0
WHERE id = ?`
).run(displayName, tiktokUsername, supporterId);
} else { } else {
supporterId = generateId(); supporterId = generateId();
db.prepare( db.prepare(
`INSERT INTO supporter_users (id, display_name, tiktok_username, email, email_verified, created_at) `INSERT INTO supporter_users
(id, display_name, tiktok_username, email, email_verified, created_at)
VALUES (?, ?, ?, ?, 0, ?)` VALUES (?, ?, ?, ?, 0, ?)`
).run(supporterId, displayName, tiktokUsername, email, nowIso()); ).run(supporterId, displayName, tiktokUsername, email, nowIso());
logAction("system", "supporter.registered", supporterId, { email }); logAction("system", "supporter.registered", supporterId, { email });
} }
// Anders als die Willkommens-E-Mail ist DIESE Mail ein echter Blocker:
// ohne den Code kommt niemand weiter. Schlägt der Versand fehl, muss das
// sichtbar scheitern statt so zu tun, als sei alles in Ordnung.
const code = await issueAuthCode(supporterId, "verify_email"); const code = await issueAuthCode(supporterId, "verify_email");
try { try {
await sendVerifyEmailCode({ to: email, name: displayName, code }); await sendVerifyEmailCode({ to: email, name: displayName, code });
} catch (err) { } catch (err) {
return emailErrorResponse(res, err); return emailErrorResponse(res, err);
} }
return json(res, { ok: true, needsVerification: true });
return json(res, { ok: true });
}
/* Einloggen mit E-Mail + selbst gewähltem Code — der neue Normalweg.
Schutz gegen Durchprobieren: nach mehreren Fehlversuchen ist die
Kombination für eine Weile gesperrt (dieselbe Mechanik wie beim
Team-Login, siehe lib/auth.js). Der Schlüssel enthält bewusst die
E-Mail-Adresse, damit ein Angreifer nicht durch Sperren FREMDER Konten
Schaden anrichten kann, ohne deren Adresse zu kennen. */
export async function loginSupporterMitCode(req, res) {
const body = req.body || {};
const email = normalizeEmail(body.email);
const accessCode = String(body.accessCode || "");
if (!email || !accessCode) return bad(res, "Bitte E-Mail-Adresse und Zugangscode eingeben.");
const sperrSchluessel = `supporter:${email}`;
if (isLockedOut(sperrSchluessel)) {
return bad(res, "Zu viele Fehlversuche. Bitte versuch es in ein paar Minuten noch einmal.", 429);
}
const supporter = db
.prepare(`SELECT id, display_name, access_code_hash FROM supporter_users WHERE email = ? AND status = 'active'`)
.get(email);
// Konto existiert, wurde aber vor der Umstellung angelegt und hat noch
// keinen eigenen Code -> gezielter Hinweis statt "falsche Daten".
if (supporter && !supporter.access_code_hash) {
return json(
res,
{ ok: false, code: "KEIN_CODE_GESETZT", error: "Für dieses Konto ist noch kein Zugangscode festgelegt. Nutze bitte „Code vergessen?“, um dir einen zu setzen." },
409
);
}
const passt = supporter ? await verifyAccessCode(accessCode, supporter.access_code_hash) : false;
if (!passt) {
recordFailedAttempt(sperrSchluessel);
// Bewusst dieselbe Meldung, egal ob die E-Mail unbekannt ist oder nur
// der Code nicht stimmt — sonst könnte man von außen durchprobieren,
// welche Adressen überhaupt registriert sind.
return bad(res, "E-Mail-Adresse oder Zugangscode stimmt nicht.", 401);
}
clearAttempts(sperrSchluessel);
db.prepare(`UPDATE supporter_users SET last_login_at = ? WHERE id = ?`).run(nowIso(), supporter.id);
const token = await createSupporterSession(supporter.id);
return json(res, { ok: true, token });
}
/* Neuen Zugangscode setzen — für eingeloggte Leute (freiwillig ändern) UND
als letzter Schritt des "Code vergessen?"-Weges (dort ist man durch den
E-Mail-Einmalcode bereits eingeloggt). */
export async function setSupporterAccessCode(req, res, supporter) {
const body = req.body || {};
const neuerCode = String(body.accessCode || "");
const fehler = pruefeCode(neuerCode);
if (fehler) return bad(res, fehler);
// Zusätzliche Absicherung (21.08.2026): Ein Zugangscode darf NUR gesetzt
// werden, wenn die E-Mail-Adresse bestätigt ist. Aktuell kann ohne
// Bestätigung ohnehin keine Session entstehen (registerSupporter gibt kein
// Token mehr zurück) -- diese Prüfung hängt aber nicht davon ab, dass das
// so bleibt. Ohne sie wäre der Bestätigungsschritt nur im Formular
// versteckt und ließe sich mit einem direkten Aufruf überspringen.
const stand = db.prepare(`SELECT email_verified FROM supporter_users WHERE id = ?`).get(supporter.id);
if (!stand || !stand.email_verified) {
return bad(res, "Bitte bestätige zuerst deine E-Mail-Adresse.", 403);
}
const codeHash = await hashAccessCode(neuerCode);
db.prepare(`UPDATE supporter_users SET access_code_hash = ?, access_code_set_at = ? WHERE id = ?`).run(codeHash, nowIso(), supporter.id);
clearAttempts(`supporter:${supporter.email}`);
logAction("system", "supporter.access_code_set", supporter.id, null);
return json(res, { ok: true });
} }
export async function verifySupporterEmail(req, res) { export async function verifySupporterEmail(req, res) {
@@ -92,20 +225,38 @@ export async function verifySupporterEmail(req, res) {
db.prepare(`UPDATE supporter_users SET email_verified = 1, last_login_at = ? WHERE id = ?`).run(nowIso(), supporter.id); db.prepare(`UPDATE supporter_users SET email_verified = 1, last_login_at = ? WHERE id = ?`).run(nowIso(), supporter.id);
logAction("system", "supporter.email_verified", supporter.id, null); logAction("system", "supporter.email_verified", supporter.id, null);
// Willkommens-E-Mail erst hier (21.08.2026) -- vorher ging sie schon beim
// Registrieren raus, also an eine noch ungeprüfte Adresse. Jetzt steht
// fest, dass die Adresse wirklich der Person gehört. Weiterhin
// nice-to-have: schlägt sie fehl, blockiert das die Anmeldung nicht.
try {
await sendWelcomeMail({ to: email, name: supporter.display_name });
} catch (err) {
logAction("system", "supporter.welcome_mail_failed", supporter.id, { fehler: String(err?.message || err).slice(0, 200) });
}
const token = await createSupporterSession(supporter.id); const token = await createSupporterSession(supporter.id);
return json(res, { ok: true, token }); return json(res, { ok: true, token });
} }
/* Seit 21.08.2026 ist das NICHT mehr der normale Login-Weg, sondern der
"Code vergessen?"-Weg: hierüber bekommt man einen Einmalcode per E-Mail,
ist danach eingeloggt und kann sich einen neuen dauerhaften Zugangscode
setzen (siehe setSupporterAccessCode). */
export async function requestSupporterLogin(req, res) { export async function requestSupporterLogin(req, res) {
const body = req.body || {}; const body = req.body || {};
const email = normalizeEmail(body.email); const email = normalizeEmail(body.email);
if (!email) return bad(res, "Bitte gib deine E-Mail-Adresse an."); if (!email) return bad(res, "Bitte gib deine E-Mail-Adresse an.");
const supporter = db const supporter = db
.prepare(`SELECT id, display_name, email_verified FROM supporter_users WHERE email = ? AND status = 'active'`) .prepare(`SELECT id, display_name FROM supporter_users WHERE email = ? AND status = 'active'`)
.get(email); .get(email);
// Bewusst IMMER ok:true nach außen (keine Auskunft, ob ein Konto existiert). // Bewusst IMMER ok:true nach außen (keine Auskunft, ob ein Konto existiert).
if (!supporter || !supporter.email_verified) return json(res, { ok: true }); // Die frühere zusätzliche Bedingung "nur wenn email_verified" ist hier
// entfallen: genau die Konten aus der Zeit vor der Umstellung (die nie
// bestätigt wurden und noch keinen eigenen Code haben) brauchen diesen Weg
// am dringendsten — sonst kämen sie gar nicht mehr rein.
if (!supporter) return json(res, { ok: true });
const code = await issueAuthCode(supporter.id, "login"); const code = await issueAuthCode(supporter.id, "login");
try { try {
+60 -2
View File
@@ -27,12 +27,27 @@ function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403); return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
} }
/* Erlaubte Auswahlbilder (21.08.2026). Muss mit der Liste in
assets/js/data-stimmen-avatare.js übereinstimmen.
Warum überhaupt eine Liste auf dem Server? Weil /testimonials/submit
öffentlich und ohne Login erreichbar ist. Ohne diese Prüfung könnte man
in das Feld einen beliebigen Text schreiben (z.B. eine fremde Bild-
Adresse oder etwas, das später als HTML/Adresse ausgewertet wird) und so
auf einer freigegebenen Stimme fremde Inhalte einbetten. Es werden
deshalb ausschließlich diese festen Ids angenommen. */
const ERLAUBTE_AVATARE = [
"husky-hoodie", "hase-hoodie", "husky-kopf", "husky-kosmos",
"hase-kosmos", "casper", "casper-blick", "logo",
];
function oeffentlichesFeld(row) { function oeffentlichesFeld(row) {
return { return {
id: row.id, id: row.id,
name: row.name, name: row.name,
tiktokUsername: row.tiktok_username, tiktokUsername: row.tiktok_username,
nachricht: row.nachricht, nachricht: row.nachricht,
avatar: row.avatar || null,
}; };
} }
@@ -42,6 +57,7 @@ function vollesFeld(row) {
name: row.name, name: row.name,
tiktokUsername: row.tiktok_username, tiktokUsername: row.tiktok_username,
nachricht: row.nachricht, nachricht: row.nachricht,
avatar: row.avatar || null,
status: row.status, status: row.status,
createdAt: row.created_at, createdAt: row.created_at,
entschiedenAm: row.entschieden_am, entschiedenAm: row.entschieden_am,
@@ -63,15 +79,57 @@ export async function submitTestimonial(req, res) {
if (!nachricht) return json(res, { ok: false, error: "Bitte schreib eine Nachricht." }, 400); if (!nachricht) return json(res, { ok: false, error: "Bitte schreib eine Nachricht." }, 400);
if (nachricht.length < 10) return json(res, { ok: false, error: "Die Nachricht ist noch sehr kurz -- magst du noch etwas mehr schreiben?" }, 400); if (nachricht.length < 10) return json(res, { ok: false, error: "Die Nachricht ist noch sehr kurz -- magst du noch etwas mehr schreiben?" }, 400);
// Selbst gewähltes Auswahlbild. Unbekannte Werte werden still verworfen
// (statt die Einreichung abzulehnen) — die Nachricht ist das Wichtige,
// und ein fehlendes Bild ist kein Grund, jemanden abzuweisen.
const gewaehlt = String(body.avatar || "").trim();
const avatar = ERLAUBTE_AVATARE.includes(gewaehlt) ? gewaehlt : null;
const id = generateId(); const id = generateId();
db.prepare( db.prepare(
`INSERT INTO testimonials (id, name, tiktok_username, nachricht, status, created_at) VALUES (?, ?, ?, ?, 'eingereicht', ?)` `INSERT INTO testimonials (id, name, tiktok_username, nachricht, avatar, status, created_at) VALUES (?, ?, ?, ?, ?, 'eingereicht', ?)`
).run(id, name, tiktokUsername, nachricht, nowIso()); ).run(id, name, tiktokUsername, nachricht, avatar, nowIso());
logAction("anonym", "testimonial.submitted", id, { name }); logAction("anonym", "testimonial.submitted", id, { name });
return json(res, { ok: true }); return json(res, { ok: true });
} }
/** Verwaltung: Profilbild einer Stimme setzen oder entfernen.
Erlaubt hier ZUSÄTZLICH zu den Auswahlbildern eine hochgeladene Datei
(siehe uploadTestimonialAvatar) — anders als beim öffentlichen Formular
ist hier eine angemeldete, berechtigte Person am Werk. */
export async function setTestimonialAvatar(req, res, session) {
if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res);
const id = String(req.body?.id || "");
const roh = String(req.body?.avatar || "").trim();
let avatar = null;
if (roh) {
const istEigenesFoto = roh.startsWith("https://postfach.dogfather-universe.com/uploads/testimonials/");
if (!ERLAUBTE_AVATARE.includes(roh) && !istEigenesFoto) {
return json(res, { ok: false, error: "Unbekanntes Profilbild." }, 400);
}
avatar = roh;
}
const info = db.prepare(`UPDATE testimonials SET avatar = ? WHERE id = ?`).run(avatar, id);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "testimonial.avatar_changed", id, { avatar });
return json(res, { ok: true });
}
/** NUR Datei-Upload (multipart, siehe index.js). Für den Fall, dass jemand
Dogi sein Foto auf anderem Weg geschickt hat (TikTok, WhatsApp …) —
das öffentliche Formular selbst nimmt bewusst KEINE Dateien an, sonst
könnte ohne Anmeldung beliebiges Material hochgeladen werden. */
export async function uploadTestimonialAvatar(req, res, session) {
if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res);
if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400);
const url = `https://postfach.dogfather-universe.com/uploads/testimonials/${req.file.filename}`;
logAction(session.actor, "testimonial.avatar_uploaded", req.file.filename, null);
return json(res, { ok: true, url });
}
/** Öffentlich, keine Session: nur freigegebene Stimmen, neueste zuerst. */ /** Öffentlich, keine Session: nur freigegebene Stimmen, neueste zuerst. */
export async function getApprovedTestimonialsPublic(req, res) { export async function getApprovedTestimonialsPublic(req, res) {
const rows = db const rows = db
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Was die Community und das Team über DogFather sagen." /> <meta name="twitter:description" content="Was die Community und das Team über DogFather sagen." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-stimmen.jpg');--page-bg-mobile:url('/assets/img/bg-stimmen-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-stimmen.jpg');--page-bg-mobile:url('/assets/img/bg-stimmen-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -87,8 +87,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-stimmen.js?v=20260821n"></script> <script src="assets/js/i18n-stimmen.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script src="assets/js/stimmen.js?v=20260821n"></script> <script src="assets/js/stimmen.js?v=20260821o"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die persönliche Geschichte hinter DogFather — Filipe, Streamer, Manager bei Spicy Media und Hundepapa von Husky Casper." /> <meta name="twitter:description" content="Die persönliche Geschichte hinter DogFather — Filipe, Streamer, Manager bei Spicy Media und Hundepapa von Husky Casper." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamer.jpg');--page-bg-mobile:url('/assets/img/bg-streamer-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamer.jpg');--page-bg-mobile:url('/assets/img/bg-streamer-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -217,7 +217,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-streamer.js?v=20260821n"></script> <script src="assets/js/i18n-streamer.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Wann geht DogFather live? Streamplan und Livebereich." /> <meta name="twitter:description" content="Wann geht DogFather live? Streamplan und Livebereich." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamplan.jpg?v=20260821n');--page-bg-mobile:url('/assets/img/bg-streamplan-mobile.jpg?v=20260821n');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamplan.jpg?v=20260821o');--page-bg-mobile:url('/assets/img/bg-streamplan-mobile.jpg?v=20260821o');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821n" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821o" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="sp_eyebrow">Welt 1 — DogFather</span> <span class="eyebrow" data-i18n="sp_eyebrow">Welt 1 — DogFather</span>
<h1 data-i18n="sp_h1">Streamplan &amp; Livebereich</h1> <h1 data-i18n="sp_h1">Streamplan &amp; Livebereich</h1>
@@ -126,8 +126,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-streamplan.js?v=20260821n"></script> <script src="assets/js/i18n-streamplan.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script src="assets/js/streamplan.js?v=20260821n"></script> <script src="assets/js/streamplan.js?v=20260821o"></script>
</body> </body>
</html> </html>
+170 -23
View File
@@ -9,10 +9,10 @@
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0b0d10" /> <meta name="theme-color" content="#0b0d10" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* ===================================================================== /* =====================================================================
Supporter-Bereich — komplett neu gestaltet (Nutzer-Feedback Supporter-Bereich — komplett neu gestaltet (Nutzer-Feedback
@@ -58,6 +58,80 @@
.poll-bar-track { height: 12px; border-radius: 999px; background: rgba(255,255,255,.06); overflow: hidden; border: 1px solid var(--border); } .poll-bar-track { height: 12px; border-radius: 999px; background: rgba(255,255,255,.06); overflow: hidden; border: 1px solid var(--border); }
.poll-bar-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent-2), var(--accent)); transition: width .4s ease; } .poll-bar-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent-2), var(--accent)); transition: width .4s ease; }
/* =====================================================================
Abschnitts-Kopf + Abmelde-Knopf (21.08.2026, Nutzer-Wunsch: "ich will
da einen richtig geilen abmelde button").
Farbwahl bewusst NICHT Gold: Gold ist auf der ganzen Seite die Farbe
für "das ist deine Belohnung / das willst du" (Prämien, Preis, Abo).
Ausloggen ist das Gegenteil — ein Weggehen. Deshalb ein gedecktes
Rosé/Rot aus derselben Edelstein-Familie wie die Verwaltungsseite:
eindeutig unterscheidbar, aber nicht das grelle Warnrot einer
Fehlermeldung (Projektregel "augenschonend"). Der Glanzstreifen läuft
nur beim Überfahren durch, nicht dauerhaft — ein permanent blinkender
Knopf in der Seitenmitte würde beim Lesen stören.
===================================================================== */
.supp-abschnitt-kopf {
display: flex; align-items: center; justify-content: space-between;
gap: 1rem 1.4rem; flex-wrap: wrap;
/* Nutzer-Hinweis 21.08.2026: "der abstand soll perfekter sein, teilweise
berühren die sich wenn man mit der maus über die kacheln geht."
Ursache war das margin:0 am h2 unten — dadurch lag der Kopf direkt auf
der Karte. Der reine Kasten-Abstand reicht hier aber nicht als Maß:
der Knopf wirft beim Überfahren einen Schatten von rund 32px nach
unten, und die Kacheln heben sich ihrerseits 4px an und leuchten
26px weit nach oben — beides zusammen berührte sich. Nachgemessen:
mit diesem Wert bleiben auch im ungünstigsten Fall (beide Hover-
Zustände gleichzeitig) deutlich sichtbare Luft dazwischen. */
margin: 0 0 1.9rem;
}
/* margin:0 nötig, damit die Überschrift in der Flex-Zeile sauber auf einer
Linie mit dem Knopf sitzt — der Abstand nach unten kommt stattdessen vom
Container oben. */
.supp-abschnitt-kopf h2 { margin: 0; }
.supp-logout {
position: relative; isolation: isolate; overflow: hidden;
display: inline-flex; align-items: center; gap: .55rem;
padding: .72rem 1.35rem; border-radius: 999px;
font-weight: 800; font-size: .92rem; letter-spacing: .01em;
cursor: pointer; white-space: nowrap;
color: #ffd9df;
border: 1.5px solid rgba(255,138,164,.45);
background: linear-gradient(140deg, rgba(74,26,38,.95), rgba(38,20,26,.95));
box-shadow: 0 8px 22px -14px rgba(255,110,140,.8), inset 0 1px 0 rgba(255,255,255,.06);
transition: transform .18s ease, box-shadow .22s ease, border-color .22s ease, color .22s ease;
}
/* Durchlaufender Glanz — startet außerhalb und wandert beim Hover einmal
quer über den Knopf. */
.supp-logout-glanz {
position: absolute; inset: 0; z-index: 1; pointer-events: none;
background: linear-gradient(100deg, transparent 34%, rgba(255,255,255,.42) 50%, transparent 66%);
background-size: 260% 100%;
background-position: 140% 0;
transition: background-position .75s ease;
}
.supp-logout > *:not(.supp-logout-glanz) { position: relative; z-index: 2; }
.supp-logout-icon { font-size: 1.02rem; line-height: 1; }
.supp-logout:hover {
transform: translateY(-2px);
color: #fff;
border-color: rgba(255,158,180,.85);
box-shadow: 0 14px 30px -12px rgba(255,110,140,.75), 0 0 22px rgba(255,110,140,.28), inset 0 1px 0 rgba(255,255,255,.1);
}
.supp-logout:hover .supp-logout-glanz { background-position: -60% 0; }
.supp-logout:active { transform: translateY(0) scale(.985); }
.supp-logout:focus-visible { outline: 2px solid rgba(255,158,180,.9); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
.supp-logout, .supp-logout-glanz { transition: none; }
.supp-logout:hover { transform: none; }
}
@media (max-width: 520px) {
/* Auf schmalen Handys volle Breite statt gequetscht neben der Überschrift. */
.supp-abschnitt-kopf { align-items: stretch; }
.supp-logout { width: 100%; justify-content: center; }
}
/* Gemeinsamer Karten-Rahmen mit Gold-Schimmerlinie oben — dieselbe /* Gemeinsamer Karten-Rahmen mit Gold-Schimmerlinie oben — dieselbe
Machart wie .abo-preis-box auf abonnieren.html, damit der Bereich Machart wie .abo-preis-box auf abonnieren.html, damit der Bereich
erkennbar zusammengehört. */ erkennbar zusammengehört. */
@@ -322,12 +396,12 @@
} }
</style> </style>
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-abonnieren.jpg');--page-bg-mobile:url('/assets/img/bg-abonnieren-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-supporter.jpg');--page-bg-mobile:url('/assets/img/bg-supporter-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-abonnieren.jpg" alt="Dogfather Supporter-Abo" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-supporter.jpg" alt="Dogfather Supporter-Abo" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="su_eyebrow">👑 Supporter-Bereich</span> <span class="eyebrow" data-i18n="su_eyebrow">👑 Supporter-Bereich</span>
<h1 id="supp-begruessung" data-i18n="su_lade">Dein Supporter-Bereich</h1> <h1 id="supp-begruessung" data-i18n="su_lade">Dein Supporter-Bereich</h1>
@@ -351,7 +425,20 @@
<p class="abo-fehler" id="poll-fehler"></p> <p class="abo-fehler" id="poll-fehler"></p>
</div> </div>
<h2 data-i18n="su_fenster1_h2">Mein Abo</h2> <!-- Nutzer-Wunsch 21.08.2026: "ich will da einen richtig geilen abmelde
button." Sitzt bewusst HIER oben neben der Überschrift statt wie
vorher ganz unten unter "Abo verwalten" — dort war er als schlichter
Umriss-Knopf kaum zu finden. Der alte wurde entfernt statt zusätzlich
stehen zu lassen: zwei Abmelde-Knöpfe auf einer Seite wären genau die
Dopplung, die schon bei "2 mal registrieren" gestört hat. -->
<div class="supp-abschnitt-kopf">
<h2 data-i18n="su_fenster1_h2">Mein Abo</h2>
<button type="button" class="supp-logout" id="btn-logout">
<span class="supp-logout-glanz" aria-hidden="true"></span>
<span class="supp-logout-icon" aria-hidden="true">⏻</span>
<span class="supp-logout-text" data-i18n="su_btn_logout">Ausloggen</span>
</button>
</div>
<div class="card supp-card"> <div class="card supp-card">
<div class="supp-stats"> <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_status">Status</div><div class="value" id="stat-status">–</div></div></div>
@@ -360,9 +447,20 @@
<div class="supp-stat supp-stat-clickable" id="stat-card-monate" tabindex="0" role="button" aria-haspopup="dialog"><span class="supp-stat-icon" aria-hidden="true">🗓️</span><div><div class="label" data-i18n="su_stat_monate">Bezahlte Monate gesamt</div><div class="value" id="stat-monate">–</div><div class="supp-stat-hinweis" data-i18n="su_stat_details_hinweis">Details ansehen</div></div></div> <div class="supp-stat 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 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>
<!-- Kein aktives Abo. Der frühere Knopf "Jetzt abonnieren" führte auf
abonnieren.html — die schickt eingeloggte Leute seit 21.08.2026
aber direkt wieder hierher zurück (siehe pruefeStatus() dort), das
wäre also eine Schleife gewesen. Solange die Bezahlung ohnehin
gesperrt ist, steht hier deshalb derselbe "Coming soon…"-Hinweis
wie auf der Anmeldeseite — ehrlicher und ohne Sackgasse. Sobald
PayPal live ist, wird hieraus wieder ein echter Knopf. -->
<div id="supp-keine-abo" style="display:none;margin-top:1.2rem;"> <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> <p class="small" data-i18n="su_kein_abo_text">Du hast noch kein aktives Abo.</p>
<a class="btn btn-primary" href="abonnieren.html" data-i18n="su_kein_abo_btn">Jetzt abonnieren</a> <div class="btn-gold-locked" style="max-width:320px;margin-top:.6rem;cursor:default;">
<span class="btn-gold-locked-lock" aria-hidden="true">🔒</span>
<span class="btn-gold-locked-text" data-i18n="su_kein_abo_soon">Coming soon…</span>
</div>
<p class="small muted" style="margin-top:.7rem;" data-i18n="su_kein_abo_hinweis">Alles hier ist schon für dich vorbereitet — sobald das Abo startet, kannst du es mit einem Klick aktivieren.</p>
</div> </div>
</div> </div>
@@ -388,9 +486,10 @@
<button type="button" class="btn btn-outline" id="btn-save-profile" data-i18n="su_btn_profil_speichern">Speichern</button> <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> <p class="abo-erfolg" id="profil-erfolg" data-i18n="su_profil_gespeichert">Gespeichert.</p>
<!-- "Ausloggen" ist von hier nach oben neben die Überschrift "Mein Abo"
gewandert (21.08.2026) — siehe dort. -->
<div class="btn-row" style="margin-top:1.6rem;"> <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-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> </div>
<p class="abo-fehler" id="verwalten-fehler"></p> <p class="abo-fehler" id="verwalten-fehler"></p>
</div> </div>
@@ -416,9 +515,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-supporter.js?v=20260821n"></script> <script src="assets/js/i18n-supporter.js?v=20260821o"></script>
<script src="assets/js/supporter.js?v=20260821n"></script> <script src="assets/js/supporter.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
const S = window.DogiSupporter; const S = window.DogiSupporter;
@@ -550,13 +649,29 @@
document.getElementById("supp-begruessung").textContent = `Willkommen, ${data.profile.displayName}!`; document.getElementById("supp-begruessung").textContent = `Willkommen, ${data.profile.displayName}!`;
const sub = data.subscription; const sub = data.subscription;
/* Läuft überhaupt ein Abo? Entscheidet, ob der nächste Meilenstein als
"läuft schon" (✨ + Countdown) oder als gesperrt (🔒) dargestellt wird.
Dieselbe Statusliste wie beim Nav-Knopf in main.js — "cancelled" zählt
bewusst noch dazu, weil der Zugang bis zum Ende des bezahlten Zeitraums
bestehen bleibt und in dieser Zeit auch noch Monate dazukommen können. */
const aboLaeuft = ["active", "pending", "suspended", "cancelled"].includes(sub.status);
document.getElementById("stat-status").innerHTML = document.getElementById("stat-status").innerHTML =
`<span class="supp-badge ${sub.status}">${STATUS_LABELS[sub.status] || sub.status}</span>`; `<span class="supp-badge ${sub.status}">${STATUS_LABELS[sub.status] || sub.status}</span>`;
document.getElementById("stat-preis").textContent = sub.monthlyPrice; /* Nutzer-Wunsch 21.08.2026: "da soll noch kein preis stehen bitte."
Der Preis ist überall sonst bewusst noch verborgen (auf abonnieren.html
steht an seiner Stelle "Coming soon…", siehe .abo-preis-zahl dort) —
über diese Kachel wäre er sonst durch die Hintertür doch schon
öffentlich gewesen. Wer bereits zahlt, MUSS seinen Preis dagegen sehen
können, deshalb nur ausblenden solange kein Abo läuft. */
document.getElementById("stat-preis").textContent = aboLaeuft ? sub.monthlyPrice : "Coming soon…";
document.getElementById("stat-naechste").textContent = fmtDatum(sub.nextPaymentAt); document.getElementById("stat-naechste").textContent = fmtDatum(sub.nextPaymentAt);
document.getElementById("stat-monate").textContent = sub.totalPaidMonths; document.getElementById("stat-monate").textContent = sub.totalPaidMonths;
document.getElementById("stat-zyklus").textContent = `Zyklus ${data.progress.cycleNumber}`; document.getElementById("stat-zyklus").textContent = `Zyklus ${data.progress.cycleNumber}`;
document.getElementById("supp-keine-abo").style.display = sub.status === "none" ? "" : "none"; document.getElementById("supp-keine-abo").style.display = sub.status === "none" ? "" : "none";
// Nutzer-Wunsch 21.08.2026: Kündigen-Knopf ergibt nur Sinn, wenn
// überhaupt ein (noch nicht schon gekündigtes) Abo existiert -- sonst
// sieht man einen Knopf für etwas, das es gar nicht gibt.
document.getElementById("btn-cancel").style.display = ["none", "cancelled", "expired"].includes(sub.status) ? "none" : "";
if (sub.status === "cancelled") { if (sub.status === "cancelled") {
const hinweis = document.getElementById("supp-kuendigungs-hinweis"); const hinweis = document.getElementById("supp-kuendigungs-hinweis");
@@ -571,10 +686,17 @@
const zeitleisteEl = document.getElementById("supp-zeitleiste"); const zeitleisteEl = document.getElementById("supp-zeitleiste");
zeitleisteEl.innerHTML = t.milestones zeitleisteEl.innerHTML = t.milestones
.map((m) => { .map((m) => {
const klasse = m.reached ? "erreicht" : m.isNext ? "naechste" : ""; // Nutzer-Wunsch 21.08.2026: "die erste kiste soll blockiert sein wie
const status = m.reached ? "Freigeschaltet" : m.isNext ? `Noch ${m.monthsRemaining} Monate` : "Gesperrt"; // die anderen, denn das ist ja verbunden mit dem abo." Ohne laufendes
// Abo läuft auch kein Countdown -- die Rechnung "noch 6 Monate" wäre
// schlicht falsch, weil der erste dieser Monate noch gar nicht
// angefangen hat. Deshalb: ohne Abo ist ALLES gesperrt, inklusive des
// ersten Meilensteins. Siehe aboLaeuft weiter oben.
const naechste = m.isNext && aboLaeuft;
const klasse = m.reached ? "erreicht" : naechste ? "naechste" : "";
const status = m.reached ? "Freigeschaltet" : naechste ? `Noch ${m.monthsRemaining} Monate` : "Gesperrt";
return `<div class="supp-meilenstein ${klasse}"> return `<div class="supp-meilenstein ${klasse}">
<div class="icon-medaille">${m.reached ? "🎁" : m.isNext ? "✨" : "🔒"}</div> <div class="icon-medaille">${m.reached ? "🎁" : naechste ? "✨" : "🔒"}</div>
<div class="monat">Monat ${m.month}</div> <div class="monat">Monat ${m.month}</div>
<div class="titel">${REDEEM_LABELS[m.type]}</div> <div class="titel">${REDEEM_LABELS[m.type]}</div>
<div class="supp-premium-status">${status}</div> <div class="supp-premium-status">${status}</div>
@@ -775,6 +897,9 @@
function modalZahlungHtml(data) { function modalZahlungHtml(data) {
const sub = data.subscription; const sub = data.subscription;
// Gleiche Regel wie auf der Preis-Kachel: ohne laufendes Abo bleibt der
// Preis verborgen (siehe Kommentar bei stat-preis).
const aboLaeuftZahlung = ["active", "pending", "suspended", "cancelled"].includes(sub.status);
const payments = data.payments || []; const payments = data.payments || [];
const gesamtBezahlt = payments const gesamtBezahlt = payments
.filter((p) => p.payment_status === "completed") .filter((p) => p.payment_status === "completed")
@@ -801,7 +926,10 @@
</div> </div>
<div class="supp-modal-grid"> <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">Nächste Zahlung</div><div class="supp-modal-value">${fmtDatum(sub.nextPaymentAt)}</div></div>
<div><div class="supp-modal-label">Betrag</div><div class="supp-modal-value">${sub.monthlyPrice}</div></div> <!-- Preis auch hier verbergen, solange kein Abo läuft (21.08.2026) —
sonst wäre er über den Klick auf "Nächste Zahlung" doch wieder
sichtbar, obwohl er auf der Kachel daneben schon verborgen ist. -->
<div><div class="supp-modal-label">Betrag</div><div class="supp-modal-value">${aboLaeuftZahlung ? sub.monthlyPrice : "Coming soon…"}</div></div>
<div><div class="supp-modal-label">Abo läuft seit</div><div class="supp-modal-value">${fmtDatum(sub.startedAt)}</div></div> <div><div class="supp-modal-label">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> <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.cancelledAt ? `<div><div class="supp-modal-label">Gekündigt am</div><div class="supp-modal-value">${fmtDatum(sub.cancelledAt)}</div></div>` : ""}
@@ -816,15 +944,20 @@
function modalFortschrittHtml(data) { function modalFortschrittHtml(data) {
const t = data.timeline; const t = data.timeline;
const p = data.progress; const p = data.progress;
const naechster = t.milestones.find((m) => m.isNext); // Gleiche Regel wie in der Zeitleiste oben (21.08.2026): ohne laufendes
// Abo ist auch der erste Meilenstein gesperrt, sonst stünden hier und
// dort widersprüchliche Angaben.
const aboLaeuftModal = ["active", "pending", "suspended", "cancelled"].includes(data.subscription.status);
const naechster = aboLaeuftModal ? t.milestones.find((m) => m.isNext) : null;
const prozent = Math.round((t.currentCycleMonth / 30) * 100); const prozent = Math.round((t.currentCycleMonth / 30) * 100);
const meilensteineHtml = t.milestones const meilensteineHtml = t.milestones
.map((m) => { .map((m) => {
const klasse = m.reached ? "erreicht" : m.isNext ? "naechste" : ""; const naechste = m.isNext && aboLaeuftModal;
const status = m.reached ? "Freigeschaltet" : m.isNext ? `noch ${m.monthsRemaining} Monate` : "gesperrt"; const klasse = m.reached ? "erreicht" : naechste ? "naechste" : "";
const status = m.reached ? "Freigeschaltet" : naechste ? `noch ${m.monthsRemaining} Monate` : "gesperrt";
return `<div class="supp-modal-meilenstein ${klasse}"> return `<div class="supp-modal-meilenstein ${klasse}">
<span class="icon-medaille-mini" aria-hidden="true">${m.reached ? "🎁" : m.isNext ? "✨" : "🔒"}</span> <span class="icon-medaille-mini" aria-hidden="true">${m.reached ? "🎁" : naechste ? "✨" : "🔒"}</span>
<div> <div>
<div class="titel">${REDEEM_LABELS[m.type]}</div> <div class="titel">${REDEEM_LABELS[m.type]}</div>
<div class="small" style="opacity:.75;">Monat ${m.month} · ${status}</div> <div class="small" style="opacity:.75;">Monat ${m.month} · ${status}</div>
@@ -847,7 +980,12 @@
: "" : ""
} }
${ ${
naechster !aboLaeuftModal
? /* Ohne Abo darf hier NICHT "Zyklus komplett" stehen (das wäre der
Fall, wenn wirklich alle Meilensteine erreicht wären) — es hat
ja noch gar nichts angefangen. */
`<p class="small" style="margin-top:.9rem;">Alle Meilensteine sind noch gesperrt — sie starten, sobald dein Abo läuft.</p>`
: naechster
? `<p class="small" style="margin-top:.9rem;">Nächster Meilenstein: <strong>${REDEEM_LABELS[naechster.type]}</strong> in ${naechster.monthsRemaining} Monat${naechster.monthsRemaining === 1 ? "" : "en"}.</p>` ? `<p class="small" style="margin-top:.9rem;">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>` : `<p class="small" style="margin-top:.9rem;">Dieser Zyklus ist komplett — alle Prämien freigeschaltet! 🎉</p>`
} }
@@ -870,7 +1008,16 @@
// vorherige Zeile im selben Klick trotzdem schon das Overlay // vorherige Zeile im selben Klick trotzdem schon das Overlay
// sichtbar gemacht hätte. Zusätzlich try/catch um den Builder-Aufruf // sichtbar gemacht hätte. Zusätzlich try/catch um den Builder-Aufruf
// selbst, mit sichtbarer Konsolen-Meldung statt stillem Fehlschlag. // selbst, mit sichtbarer Konsolen-Meldung statt stillem Fehlschlag.
if (!letzteDaten || letzteDaten.subscription?.status === "none") return; /* Nutzer-Wunsch 21.08.2026: "sie sollen ALLES SEHEN KÖNNEN." Die
frühere Bedingung blockierte das Detail-Fenster komplett, solange
kein Abo lief — die Kacheln laden aber sichtbar mit "Details
ansehen ›" zum Klicken ein, es passierte dann nur einfach nichts.
Beide Fenster kommen mit "kein Abo" sauber zurecht: die
Zahlungsübersicht zeigt "Noch keine Zahlungen erfasst.", die
Fortschritts-Ansicht alle Meilensteine gesperrt plus passenden
Hinweis (siehe modalFortschrittHtml). Deshalb bleibt hier nur noch
die Absicherung gegen fehlende Daten. */
if (!letzteDaten) return;
try { try {
oeffneModal(builder(letzteDaten)); oeffneModal(builder(letzteDaten));
} catch (err) { } catch (err) {
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="VanVan, Diene, Funny, Miss und Marina — das Modi-/Core-Team von Team Dogi." /> <meta name="twitter:description" content="VanVan, Diene, Funny, Miss und Marina — das Modi-/Core-Team von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-modis.jpg');--page-bg-mobile:url('/assets/img/bg-modis.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-dogfather.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-modis.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="tm_eyebrow">🩵 Team Dogi</span> <span class="eyebrow" data-i18n="tm_eyebrow">🩵 Team Dogi</span>
<h1 data-i18n="tm_h1" data-site-text-key="seitentitel_modi">Das Modi-Team</h1> <h1 data-i18n="tm_h1" data-site-text-key="seitentitel_modi">Das Modi-Team</h1>
@@ -72,9 +72,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821n"></script> <script src="assets/js/data-modis.js?v=20260821o"></script>
<script src="assets/js/i18n-team-modis.js?v=20260821n"></script> <script src="assets/js/i18n-team-modis.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("modis-grid"); const grid = document.getElementById("modis-grid");
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die Scouts von Team Dogi bei Spicy Media." /> <meta name="twitter:description" content="Die Scouts von Team Dogi bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -49,9 +49,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-scouts.js?v=20260821n"></script> <script src="assets/js/data-scouts.js?v=20260821o"></script>
<script src="assets/js/i18n-team-scouts.js?v=20260821n"></script> <script src="assets/js/i18n-team-scouts.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("scouts-grid"); const grid = document.getElementById("scouts-grid");
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Eine persönliche Widmung von Dogi an Tili — seine TikTok-Muddi, die erste und bis heute einzige Streamerin, die ihm all die Zeit treu geblieben ist." /> <meta name="twitter:description" content="Eine persönliche Widmung von Dogi an Tili — seine TikTok-Muddi, die erste und bis heute einzige Streamerin, die ihm all die Zeit treu geblieben ist." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* Gleiche Anpassung wie vanvan.html: der site-weit etwas größere /* Gleiche Anpassung wie vanvan.html: der site-weit etwas größere
Fließtext wirkt bei einem so langen persönlichen Brief zu wuchtig — Fließtext wirkt bei einem so langen persönlichen Brief zu wuchtig —
@@ -153,7 +153,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-tt-muddi.js?v=20260821n"></script> <script src="assets/js/i18n-tt-muddi.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="VanVan erzählt ihre Geschichte: wie sie zu Team Dogi kam, wer sie ist und was DogFather und Team Dogi ihr bedeuten." /> <meta name="twitter:description" content="VanVan erzählt ihre Geschichte: wie sie zu Team Dogi kam, wer sie ist und was DogFather und Team Dogi ihr bedeuten." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* Auf Wunsch: der site-weit vergrößerte Fließtext wirkte auf VanVans /* Auf Wunsch: der site-weit vergrößerte Fließtext wirkte auf VanVans
eigener Seite zu groß (viel Text am Stück) — hier gezielt wieder eigener Seite zu groß (viel Text am Stück) — hier gezielt wieder
@@ -175,7 +175,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-vanvan.js?v=20260821n"></script> <script src="assets/js/i18n-vanvan.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -19,10 +19,10 @@
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon-admin.png" /> <link rel="apple-touch-icon" href="assets/img/apple-touch-icon-admin.png" />
<meta name="apple-mobile-web-app-title" content="DogiCrew-Verwaltung" /> <meta name="apple-mobile-web-app-title" content="DogiCrew-Verwaltung" />
<meta name="theme-color" content="#0e0a16" /> <meta name="theme-color" content="#0e0a16" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* ===================================================================== /* =====================================================================
verwaltung.html — Dogis privater Command-Center-Bereich. verwaltung.html — Dogis privater Command-Center-Bereich.
@@ -1207,12 +1207,12 @@
<!-- 21.08.2026: für den "Bestehende Mitglieder importieren"-Knopf in der <!-- 21.08.2026: für den "Bestehende Mitglieder importieren"-Knopf in der
Team-Verwaltung -- liest die von Hand gepflegten Profile aus diesen Team-Verwaltung -- liest die von Hand gepflegten Profile aus diesen
Dateien aus und bietet an, sie 1:1 in die Datenbank zu übernehmen. --> Dateien aus und bietet an, sie 1:1 in die Datenbank zu übernehmen. -->
<script src="assets/js/data-modis.js?v=20260821n"></script> <script src="assets/js/data-modis.js?v=20260821o"></script>
<script src="assets/js/data-scouts.js?v=20260821n"></script> <script src="assets/js/data-scouts.js?v=20260821o"></script>
<script src="assets/js/data-creator.js?v=20260821n"></script> <script src="assets/js/data-creator.js?v=20260821o"></script>
<script src="assets/js/data-manager.js?v=20260821n"></script> <script src="assets/js/data-manager.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script src="assets/js/admin-auth.js?v=20260821n"></script> <script src="assets/js/admin-auth.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com"; const API_BASE_URL = "https://postfach.dogfather-universe.com";
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die Werte, nach denen DogFather als Manager bei Spicy Media arbeitet." /> <meta name="twitter:description" content="Die Werte, nach denen DogFather als Manager bei Spicy Media arbeitet." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-werte.jpg');--page-bg-mobile:url('/assets/img/bg-werte-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-werte.jpg');--page-bg-mobile:url('/assets/img/bg-werte-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -113,7 +113,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-werte.js?v=20260821n"></script> <script src="assets/js/i18n-werte.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die Geschichte von DogFather in einer interaktiven Zeitleiste." /> <meta name="twitter:description" content="Die Geschichte von DogFather in einer interaktiven Zeitleiste." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-zeitreise.jpg');--page-bg-mobile:url('/assets/img/bg-zeitreise-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-zeitreise.jpg');--page-bg-mobile:url('/assets/img/bg-zeitreise-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -237,8 +237,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-zeitreise.js?v=20260821n"></script> <script src="assets/js/i18n-zeitreise.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821n"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
/* Wachstums-Reveal: Äste blenden sich beim Scrollen sanft ein — passt /* Wachstums-Reveal: Äste blenden sich beim Scrollen sanft ein — passt
zum "wachsenden Baum"-Konzept dieser Seite. Rein page-lokal, betrifft zum "wachsenden Baum"-Konzept dieser Seite. Rein page-lokal, betrifft