Compare commits

..
53 Commits
Author SHA1 Message Date
DogFatherGitandClaude Sonnet 5 4b294f6c6b Startseite: die vier Personen-Kacheln unter "Team Dogi" entfernt
Nutzer-Wunsch 21.08.2026: "die 4 kacheln da sollen da weg bitte."
Überschrift, Beschreibungstext und der Knopf zum Modi-Team bleiben --
dadurch kommt das "Team Dogi"-Hintergrundbild dieser Sektion jetzt
unverdeckt zur Geltung. Der zugehörige Render-Code ist ebenfalls raus;
data-modis.js/data-scouts.js bleiben eingebunden, weil die
Statistik-Zeile weiter oben sie weiterhin auszählt (geprüft: zeigt
weiterhin korrekt "8+"). Die Profile selbst gibt es unverändert
vollständig auf team-modis.html.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 16:53:56 +02:00
DogFatherGitandClaude Sonnet 5 d0f3edb079 bewerben.html: Überschrift-Umbruch verbessert (kein einsames letztes Wort mehr)
Nutzer-Wunsch 21.08.2026: "Weg" stand auf manchen Bildschirmbreiten
allein in einer eigenen Zeile, sah unruhig aus. Non-breaking-Space
zwischen den letzten zwei Wörtern in allen 5 Sprachen -- bricht die
Zeile jetzt nie mehr genau zwischen ihnen um.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 16:51:00 +02:00
DogFatherGitandClaude Sonnet 5 76f70ffcf2 Zeitreise: Namen "Cidem" zu "Cidgem" korrigiert (Creator Cup)
Nutzer-Wunsch 21.08.2026: Tippfehler beim Namen des Kooperationspartners
beim Creator Cup korrigiert, in allen 5 Sprachen plus dem
HTML-Fallbacktext.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 12:58:29 +02:00
DogFatherGitandClaude Sonnet 5 971c5ea3f6 Bugfix: Team-Foto-Upload lieferte relativen statt vollen Pfad zurück
Gleicher Fehler wie in routes/events.js/uploadEventImage (dort gerade
direkt auf dem Server gefunden und gefixt, siehe git stash auf
dogiintern): die Verwaltung und die echte Website laufen auf
dogfather-universe.com (dogiweb), hochgeladene Fotos liegen aber unter
postfach.dogfather-universe.com (dogiintern). Ein relativer Pfad wurde
vom Browser also gegen die falsche Domain aufgelöst -> kaputtes
Bild-Symbol für jedes über die Team-Verwaltung hochgeladene Foto.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 04:37:04 +02:00
DogFatherGitandClaude Sonnet 5 81f55d67db Team-Verwaltung Runde 2: bestehende Mitglieder importieren + Kategorien/Seitentitel editierbar
Nutzer-Wunsch 21.08.2026: "wenn ich eine änderung mache ist es auch für
jeden für die die schon da sind und die neuen" + "ich will ich titel
und namen von kategorien und seiten ändern können in der verwaltung".

Backend (braucht Filipes manuellen Deploy):
- Migration 0008: team_members bekommt intro/bioHtml/extraCta-Spalten
  -- volle Feld-Parität mit den von Hand gepflegten Profilen (Diene/
  Patrick/Bananenstift/Marina nutzen diese Felder).
- routes/team.js: neue importLegacyMember()-Funktion -- übernimmt ein
  bestehendes Profil 1:1 in die Datenbank, OHNE erneut zu übersetzen
  (die vorhandenen, von Hand geschriebenen Übersetzungen bleiben
  erhalten). Idempotent: mehrfacher Import erzeugt keine Duplikate.
- routes/site-texts.js (neu): admin-editierbare Kategorie-Namen und
  Seitentitel, generischer key->{de,...}-Override über app_settings
  (wie "Event des Jahres"), fester Schlüssel-Katalog aus
  Sicherheitsgründen. Leeres Feld setzt auf den Standardtext zurück.
- 27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamZusammenfuehren() (main.js): admin-Mitglieder
  ÜBERSCHREIBEN jetzt gleichnamige statische Einträge (per slug) statt
  sie zu duplizieren -- eine Bearbeitung wirkt dadurch für alle.
- window.dogiSiteTexteLaden() + applyTranslations() erweitert um
  data-site-text-key -- Kategorie-Seitentitel (team-modis.html/
  team-scouts.html/creator.html/manager.html) sind jetzt live editierbar.
- Verwaltung: "📥 Bestehende Mitglieder importieren"-Knopf (holt VanVan/
  Diene/Funny/Miss/Marina/Ghost/Patrick/Bananenstift aus den data-*.js-
  Dateien, VanVan bewusst ausgenommen -- eigene Sonderkarte + Seite),
  erweitertes Formular (Intro/ausführliche Vorstellung/zweiter Button,
  eingeklappt unter "Erweitert"), neue Sektion "Kategorien &
  Seitentitel" (4 Karten, sofort wirksam auf Website UND Verwaltung).

Beim Testen einen echten UX-Bug gefunden und gefixt: die "Gespeichert"-
Meldung nach dem Speichern eines Mitglieds wurde von der direkt
anschließenden Formular-Zurücksetzung sofort wieder überschrieben und
war nie sichtbar.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 03:47:51 +02:00
DogFatherGitandClaude Sonnet 5 7060a4a6ae Verwaltungsseite perfektioniert: Team-Verwaltung für Modis/Scouts/Creator/Manager
Nutzer-Wunsch 21.08.2026: "überall wo Modis sind oder Scouts oder
Manager, oder Creator, will ich dass ich die easy über meine
Verwaltungsseite hinzufüge und die automatisch in der Website
hinzugefügt werden ... auch mit Fotos und TikTok Link."

Backend (server-internal, braucht Filipes manuellen Deploy):
- Neue Tabelle team_members (Migration 0007) für alle vier Kategorien
  gemeinsam -- ergänzt, überschreibt NIE die von Hand gepflegten
  Einträge in data-modis.js/data-scouts.js/data-creator.js.
- routes/team.js: öffentliches Lesen (nur aktive Mitglieder, optional
  nach Kategorie gefiltert), TEAM_MANAGE-geschütztes Anlegen/Bearbeiten/
  Löschen, Foto-Upload (gleiches Muster wie "Event des Jahres"),
  automatische Übersetzung von Rolle/Bio/Zitat wie bei anderen
  admin-gepflegten Texten. Slug global eindeutig (mit automatischer
  Kollisionsauflösung), TikTok-Kurzname wird zu voller URL ergänzt.
  27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamLaden() in main.js: holt admin-gepflegte Mitglieder
  einer (oder aller) Kategorien und bringt sie in exakt dieselbe Form
  wie die bestehenden data-*.js-Einträge.
- team-scouts.html/team-modis.html/creator.html hängen das Ergebnis
  einfach an ihre bestehenden Arrays an und rendern erneut -- exakt
  derselbe Look wie die schon bestehenden Profile (Foto/Avatar-Initiale,
  Rolle, Zitat, TikTok-Button). creator.html hatte bisher ein "return"
  bei leerem CREATORS-Array, wodurch admin-Creator NIE geladen worden
  wären -- gefixt. team-modis.html berücksichtigt Rang/Rang-Bezeichnung
  für die Pyramide.
- Neue Sektion "Weitere Manager" auf manager.html, komplett unsichtbar
  bis der erste Manager angelegt wird (data-manager.js neu, wie
  data-creator.js aktuell leer).
- profil.html sucht jetzt kategorieübergreifend auch in admin-gepflegten
  Profilen, inkl. korrektem Theme/Zurück-Link auch für Manager.

Verwaltung: neue Sektion "Team verwalten" (TEAM_MANAGE-Berechtigung) --
ein Formular für alle vier Kategorien mit Foto-Sofort-Upload,
TikTok-Feld, Bio/Zitat, bedingten Rang-Feldern (nur Modi), Kategorie-
Filterleiste und Bearbeiten/Löschen pro Eintrag.

Beim Testen mit Playwright einen echten Syntaxfehler gefunden und
gefixt (ASCII-Anführungszeichen statt schließendem „" in einem
Statustext), der das GESAMTE Verwaltungs-Skript und damit die komplette
Seite lahmgelegt hätte.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 03:06:21 +02:00
DogFatherGitandClaude Sonnet 5 8ead1b750b KRITISCH: echten Ursprung der "rohen Übersetzungs-Schlüssel"-Bugs gefunden und behoben
Beide heutigen Meldungen ("pr_quote_open...", "ts_profil_ansehen") waren
KEIN Caching-Problem, sondern ein echter Bug in main.js: nach Klick auf
einen internen Link und dann "Zurück" (oder generell jede zweite Seite
innerhalb einer Browser-Sitzung) wurde die seiteneigene i18n-*.js-Datei
nie erneut ausgeführt -> window.I18N_PAGE blieb auf null -> jede
Übersetzung fiel auf den rohen Schlüsselnamen zurück.

Ursache: skripteUebernehmen() erkennt i18n-Dateien am Muster ".js$"
(String-Ende). Seit die Cache-Busting-Version ("?v=...") an jede
Skript-URL angehängt wird, endet der echte src-Wert aber nie mehr auf
".js", sondern auf ".js?v=...". Der Test schlug seitdem für JEDE
i18n-Datei fehl und sie wurde wie eine normale, schon geladene Datei
behandelt und beim nächsten Seitenwechsel übersprungen.

Mit Playwright reproduziert (Klick auf Profil-Kachel + Zurück-Button)
und nach dem Fix erneut verifiziert -- betraf praktisch jede Seite mit
eigenem i18n-*.js beim Navigieren per Klick, nicht nur team-scouts.html.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 02:12:24 +02:00
DogFatherGitandClaude Sonnet 5 fbd11b7be4 "Was gerade läuft"-Aktionen-Teaser von der Startseite entfernt
Nutzer-Wunsch 21.08.2026: Überschrift + Button "Alle Aktionen & Projekte"
auf index.html weg. data-aktionen.js-Script-Tag (nur noch dafür gebraucht)
und die zugehörigen JS-Renderzeilen ebenfalls entfernt, dazugehörige
i18n-Keys (idx_aktionen_h2/idx_aktionen_btn) aus i18n-index.js aufgeräumt.
aktion-detail.html und data-aktionen.js selbst bleiben unangetastet.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 02:04:43 +02:00
DogFatherGitandClaude Sonnet 5 ca5793e79f Patricks Profil-Abschluss um "Scout von Dogfather" ergänzt (wie Bananenstift)
Nutzer-Wunsch 21.08.2026: Patricks Bio endete nur mit "- euer Patrick",
Bananenstift hat zusätzlich eine zweite Zeile "Scout von Dogfather".
Für Konsistenz zwischen beiden Scout-Profilen in allen 5 Sprachen ergänzt.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:53:57 +02:00
DogFatherGitandClaude Sonnet 5 c7904ccd62 TikTok-Buttons auf Patrick- und Bananenstift-Scout-Profilen ergänzt
Nutzer-Wunsch 21.08.2026: unter dem Namen/Rolle-Text je ein TikTok-Button
(@patrick180585 bzw. @bananenstift009). Beide nutzen den bereits
bestehenden social-Mechanismus in profil.html (social.tiktok -> echter
.btn-tiktok-Button), kein neuer Code nötig.

Cache-Busting-Version auf 20260821f erhöht (data-scouts.js ist .js und
damit von Cloudflares erzwungenem Edge-Cache betroffen).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:49:00 +02:00
DogFatherGitandClaude Sonnet 5 ac01a74d08 Zeitreise: neue Station "Erstes Fan-Treffen: Team TiliDog" (25. Juli 2026)
Nutzer-Wunsch 21.08.2026: erstes Fan-Treffen von Dogi & Tili (Team
TiliDog) in Wuppertal ergänzt -- gleichzeitig das erste persönliche
Aufeinandertreffen von DogFather und Tili. Als Meilenstein markiert und
chronologisch korrekt zwischen "Juli 2026 — Start als Manager" und
"27. Juli bis 2. August 2026 — Creator Cup" einsortiert.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:39:47 +02:00
DogFatherGitandClaude Sonnet 5 360f19a52d Echtes Foto von Filipe in der Intro-Sektion auf streamer.html ergänzt
Nutzer-Wunsch 21.08.2026: "füge das foto was ich am ende mitschicke
neben den text im screen1". Sektion von einspaltigem Fließtext auf
grid-2 umgebaut, Foto (assets/img/streamer-filipe-portrait.jpg, aus
Pictures/Filipe, auf 1000x1500 verkleinert) rechts daneben mit
frame-gold-Rahmen für Abwechslung zur Casper-Sektion (frame-lila).

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:29:03 +02:00
DogFatherGitandClaude Sonnet 5 6106cf9f03 Modi-Gang TikTok-Button von streamer.html nach team-modis.html verschoben
Der Button gehört thematisch besser zum Modi-Team als zur Casper-Sektion
auf streamer.html (Nutzer-Wunsch 21.08.2026: "den knopf da weg bitte und
in die modie seite zu den modis hinzufügen und perfekt in die seite
anpassen"). Jetzt im Hero von team-modis.html, unter dem Lead-Text,
zentriert im bestehenden btn-row-Muster der Seite.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:24:43 +02:00
DogFatherGitandClaude Sonnet 5 ed9b373a74 Husky-Bild auf streamer.html an Texthöhe angepasst statt fixem Mini-Deckel
Vorheriger max-height-Deckel (460px) fürs sehr hochkantige
streamer-mascot.jpg (900x2812) war zu knapp und ließ eine sichtbare
Lücke neben dem längeren Text ("die größe vom bild vom husky soll dem
text rechts nebendran angepasst werden"). Deckel auf 720px erhöht
(orientiert an der typischen Texthöhe in diesem Abschnitt), Grid-Stretch-
Ansatz ausprobiert und wegen Rückkopplung mit dem extremen
Seitenverhältnis verworfen (siehe Kommentare in main.css/streamer.html).

Cache-Busting-Version auf 20260821b erhöht (Cloudflare cached CSS/JS
sonst bis zu 4h am Edge).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:21:31 +02:00
DogFatherGitandClaude Opus 5 1f87b54351 Nav umbenannt/umsortiert + Stimmen-Kachel und Zitat-Karten aufgewertet
Nutzer-Wuensche 20.08.2026 (2. Runde):
- Kategorie "DogFather" -> "Filipe", Kategorie "HasiDog" -> "Dogi&Hasi".
- Unterpunkt "Streamer" (streamer.html) aus "Filipe" heraus nach
  "Dogi&Hasi" verschoben, dort GANZ NACH OBEN (ueber "HasiDog") und in
  "DogFather" umbenannt.
- Unterpunkt "HasiDog-Welt" -> "HasiDog".
- Fusszeile zieht mit: Spaltenueberschrift nutzt jetzt dieselbe
  Kategorie-Bezeichnung wie die Nav ("Filipe" / "Dogi&Hasi & Manager"),
  streamer.html steht dort ebenfalls in der Dogi&Hasi-Spalte ganz oben.
  Die internen Schluesselnamen (nav_dogfather...) bleiben bewusst
  unveraendert -- reine Bezeichner, ein Umbenennen waere nur Fehlerquelle.

- "die kachel soll noch spezieller und geiler aussehen": Einreich-Kachel
  auf stimmen.html deutlich aufgewertet -- wandernder Farbverlauf-Rahmen
  (Zwei-Ebenen-/background-position-Technik, KEIN rotierender Ring: siehe
  dokumentierte Projekt-Lektion zu Verzerrungen auf eckigen Flaechen),
  schwebende Herz-Partikel, Medaillon statt nacktem Emoji,
  Farbverlauf-Ueberschrift, aufleuchtende Eingabefelder.
- "und auch die die danach kommen ... sollen viel geiler sein": freigegebene
  Stimmen sind jetzt echte Praesentationskarten -- farbige Akzentlinie oben,
  grosses Anfuehrungszeichen als Wasserzeichen, Avatar-Medaillon mit
  Anfangsbuchstabe, TikTok-Handle als eigener Chip, sanftes Anheben beim
  Ueberfahren.

Alles augenschonend gehalten (gedeckte Toene, langsame Bewegungen,
vollstaendiger Stillstand bei prefers-reduced-motion) und per Playwright
visuell geprueft, keine JS-Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 01:02:36 +02:00
DogFatherGitandClaude Opus 5 5146cef164 TikTok-Knoepfe fuer HasiDog + Modi-Gang, Zeitreise-Reihenfolge, Bildgroesse
Nutzer-Wuensche 20.08.2026:
- hasidog.html: TikTok-Knopf direkt unter der grossen Ueberschrift,
  Account @hasidog0804 (alle 5 Sprachen uebersetzt).
- streamer.html: TikTok-Knopf unter "Casper - mein treuer Begleiter",
  Account @dogis.modi.gang (alle 5 Sprachen uebersetzt).
- zeitreise.html: die beiden letzten Karten getauscht -- der Website-Start
  (21.08.2026, konkretes Datum) steht jetzt VOR der Teddy-Kooperation
  ("Datum folgt"), damit die Zeitleiste durchgehend chronologisch bleibt.
- streamer.html Bildgroesse gefixt ("der soll bissl kleiner sein und nicht
  so riesig"): streamer-mascot.jpg ist 900x2812 px und lief mit
  aspect-ratio:auto + height:auto in voller natuerlicher Hoehe -- rund
  dreimal so hoch wie die Textspalte daneben. Neue Klasse
  .collage-photo-kompakt deckelt die Hoehe auf 460px und laesst den Rahmen
  eng am Bild sitzen (width:fit-content), die ganze Figur bleibt sichtbar.
  Regel steht bewusst NACH ".collage-photo img" -- gleiche Spezifitaet,
  die spaetere gewinnt, sonst haette width:100%/height:100% sie aufgehoben.

Alles per Playwright verifiziert (beide Knoepfe mit korrekten Links,
Bildhoehe 460px statt ~1780px, neue Zeitreise-Reihenfolge), keine JS-Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:46:04 +02:00
DogFatherGitandClaude Opus 5 399260b805 "Stimmen" verwandelt: Fans reichen selbst Testimonials ein
Nutzer-Wunsch 20.08.2026: "eine richtig geile kachel ... wo die leute mit
namen und tiktok namen mir eine nachricht schreiben können die ich in die
verwaltung kriege, und dann kann ich die ausgewählten über die
verwaltungsseite auf die website hinzufügen ... das wird mega persoenlich
zu den fans, bitte wirklich krass geil speziell."

- Neue, augenschonend gestaltete Einreich-Kachel auf stimmen.html: Name,
  TikTok-Name (optional), Nachricht -- landet NIE automatisch oeffentlich,
  sondern immer erst als Entwurf in der Verwaltung.
  Warmer Babyblau/Rosé-Farbverlauf, wandernder Lichtschein, pulsierendes
  Herz -- ersetzt die 3 ewigen "Hier steht bald..."-Platzhalterkarten.
- Neue Verwaltungs-Sektion "💬 Stimmen verwalten": Warteschlange (offene
  zuerst), Freigeben/Ablehnen/Löschen pro Eintrag.
- Freigegebene Stimmen erscheinen automatisch in einer neuen, spezielleren
  Zitat-Kartenoptik (großes Anführungszeichen, Name + TikTok-Chip) --
  Abschnitt bleibt komplett unsichtbar, solange keine einzige freigegeben
  wurde.
- Backend: neue Tabelle `testimonials` (Migration 0006), routes/
  testimonials.js (oeffentliches Einreichen + Lesen freigegebener,
  admin-Warteschlange + Status/Loeschen mit neuer TESTIMONIALS_MANAGE-
  Berechtigung). 18 automatisierte Tests gegen eine Fake-DB bestanden.
- Alles per Playwright visuell durchgespielt: leerer Zustand, Einreichen
  -> Erfolgsmeldung, freigegebene Stimmen-Anzeige, Verwaltungs-Warteschlange
  mit allen Aktionen.

Backend-Teil (server-internal/, cloudflare-worker/migrations/) noch ohne
Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:37:31 +02:00
DogFatherGitandClaude Opus 5 519e209bce Zweite Live-Uhr fuer "spezielles Live-Event" + mehr Farbe im Radar
Nutzer-Wunsch 20.08.2026: "wuensch mir nur noch bissl farbe und sehr
wichtig ist dass ich auch spezielle live events da auch eintragen kann
in der verwaltungsseite so dass da eine zweite uhr erscheint wo die zeit
dann fuer dieses spezielle event laeuft."

- Neue Verwaltungs-Sektion "Spezielles Live-Event": Titel (Deutsch, wird
  automatisch uebersetzt), echtes Datum+Uhrzeit, optionaler Link, Aktiv-
  Schalter -- nur mit Titel + Zukunftsdatum aktivierbar.
- Zweite Uhr auf streamplan.html (Magenta/Violett statt Cyan/Gold, direkt
  neben der bestehenden), nur sichtbar wenn ein Event aktiviert ist und
  das Zieldatum noch nicht vorbei ist. Echter Countdown inkl. Tage (z.B.
  "2T 03:14:59"), "Jetzt"-Zeiger zeigt wie bei Uhr 1 die tatsaechliche
  Uhrzeit, der magentafarbene Fixpunkt markiert die Tageszeit des Events.
  Zieldatum wird als UTC gespeichert -- jede besuchende Person sieht den
  exakt richtigen Countdown in ihrer eigenen Zeitzone.
- "Bissl Farbe": bunter Farbverlauf (Cyan/Violett/Gold) auf dem aeusseren
  Ring statt reinem Grauton, kraeftigerer Sweep-Farbschein.
- Backend: neue routes/special-event.js (getSpecialEventPublic nur bei
  aktiv+zukuenftig, getSpecialEventAdmin fuer Entwuerfe, saveSpecialEvent
  mit Validierung), 13 automatisierte Tests gegen eine Fake-DB bestanden.
- Bugfix unterwegs gefunden (Playwright-Screenshot, wiederholtes Muster
  auf dieser Seite): .stream-radar-wrap blieb trotz [hidden]-Attribut
  sichtbar (display:block ueberschreibt die eingebaute [hidden]-Regel bei
  gleicher Spezifitaet) -- explizite Regel ergaenzt, per Playwright erneut
  verifiziert (display:none bestaetigt).

Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn
manuell auf dogiintern ausrollen, sonst bleibt die zweite Uhr unsichtbar.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:24:17 +02:00
DogFatherGitandClaude Opus 5 7fdceadb9e Fix: Sprachschalter-Menue nicht mittig wie die Kategorie-Menues
Nutzer-Report per Screenshot-Vergleich: "Medien"-Kategorie ist mittig
unter dem Knopf, der Sprachschalter (DE/CH/EN/FR/PT) aber rechtsbuendig --
war bewusst so gebaut, als der Schalter noch ganz am rechten Rand sass.
Seit DogiCrew-/Bewerben-Knopf daneben stehen, ist genug Platz fuer
dieselbe zentrierte Ausrichtung wie ueberall sonst -- macht die Optik
konsistent und behebt nebenbei einen Detail-Fehler: der Verbindungspfeil
zeigte schon immer mittig, waehrend die Box selbst rechts daneben sass.
Per Playwright verifiziert: Dropdown jetzt exakt unter dem Knopf zentriert,
laeuft bei keiner getesteten Breite ueber den Rand.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:10:27 +02:00
DogFatherGitandClaude Opus 5 7dadd84cbb Neue Hintergrundbilder: Startseite (Trio) + Streamplan
Nutzer-Wunsch 20.08.2026: Startseiten-Hero (universe-trio.jpg) und
Streamplan-Hero (bg-streamplan.jpg/-mobile.jpg) durch neu bereitgestellte
Bilder ersetzt. Blur-Variante fuer die Startseite neu aus dem frischen
Bild erzeugt (Gaussian Blur, passend zur bestehenden Cover-Verlauf-Technik).
Mobile-Ausschnitt fuer Streamplan mehrfach nachjustiert, damit der
"STREAMPLAN"-Schriftzug im schmalen Hochformat-Crop komplett sichtbar
bleibt. Alte Bilder als .bak-20-08-2026 gesichert (nicht eingecheckt).
Referenzen mit Versions-Stempel (?v=) versehen, damit der neue Stand
sofort sichtbar ist statt bis zu 4 Std im Cache zu haengen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:07:33 +02:00
DogFatherGitandClaude Opus 5 19636a25ec Fix: per Klick geoeffnetes Nav-Kategorie-Menue schloss sich nicht von selbst
Nutzer-Report per Screenshot: Klick auf eine Kategorie (z.B. "DogFather")
liess das Untermenue offen stehen, bis irgendwo anders hingeklickt wurde --
verliess man es einfach mit der Maus, blieb es haengen. Jetzt schliesst
sich ein per Klick geoeffnetes Menue automatisch, sobald die Maus die
Kategorie (Knopf + Untermenue) verlaesst -- nur auf echten Maus-Geraeten
(hover:hover + pointer:fine), auf Touch/Tablet aendert sich nichts, dort
gibt es kein "mit der Maus verlassen". Per Playwright verifiziert: Klick
oeffnet, Mausbewegung weg schliesst.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:56:49 +02:00
DogFatherGitandClaude Opus 5 a5615331aa Streamplan-Seite: interaktives Live-Radar statt schlichter Textkarten
Nutzer-Wunsch 20.08.2026: "diese seite vom streamplan soll viel geiler
und spezieller sein, ueberrasch mich."

Neues Herzstueck: ein 24-Stunden-Ziffernblatt (reines SVG, keine
Bild-Assets), das den festen 20-Uhr-Termin als dauerhaft leuchtenden
Fixpunkt zeigt (goldener Puls) und einen "Jetzt"-Zeiger in Echtzeit mit
der tatsaechlichen Uhrzeit der besuchenden Person mitbewegt (Babyblau,
DogFather-Markenfarbe). Nutzt denselben /live-status-Endpunkt wie die
Startseite:
- Offline: echter Sekunden-Countdown zum naechsten lokalen 20-Uhr-Termin.
- Live: komplettes Radar schaltet auf Rot/Puls um, zeigt den Stream-Titel
  und einen "Jetzt anschauen"-Knopf direkt zu TikTok.
Sanft rotierender Lichtschein fuer Atmosphaere, komplett augenschonend
(gedeckte Farben, respektiert prefers-reduced-motion vollstaendig).

Bugfix unterwegs gefunden (Playwright-Screenshot, dritte Wiederholung
desselben Musters heute): der "Jetzt anschauen"-Knopf blieb trotz
[hidden]-Attribut sichtbar, weil .btn selbst "display" setzt und damit
gleiche Spezifitaet wie die eingebaute [hidden]-Regel hat -- explizite
Regel ergaenzt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:50:49 +02:00
DogFatherGitandClaude Opus 5 b3e191d3ca "Event des Jahres": Aktivieren-Schalter, leer bis befuellt, Detail-Fenster
Nutzer-Wunsch 20.08.2026: "diese zwei kisten sollen leer sein solang wie
ich nichts rein setze, am besten sollen die sogar immer nur erscheinen
wenn ich was rein setze und sie aktiviere ... wen ich drauf druecke dass
dann ein kleines fenster aufgeht wo das bild bissl groesser ist mit einem
groesseren text und beschreibung, und mit einem link ... die kacheln
sollen auch bissl spezieller sein."

- Kein hartkodierter Standardinhalt mehr auf der Startseite -- die Kachel
  UND der ganze Abschnitt bleiben komplett unsichtbar, bis mindestens ein
  Event in der Verwaltung ausgefuellt UND ueber einen neuen "Aktiv"-Schalter
  freigeschaltet ist. Genau 1 aktives Event -> zentrierte Einzelkachel
  statt halbleerem Zwei-Spalten-Raster.
- Klick auf eine Kachel oeffnet jetzt ein Detail-Fenster (groesseres Bild,
  groesserer Titel/Text, optionaler direkter Link) statt sofort
  wegzunavigieren.
- Kacheln bekommen einen goldenen Trophaeen-Akzent + dezenten wandernden
  Lichtschimmer statt der neutralen Standardkarten-Optik.
- Bugfix unterwegs gefunden (Playwright-Screenshot): .jahres-event-card
  blieb trotz [hidden]-Attribut sichtbar (dieselbe Ursache wie der
  frühere .jahres-event-bild-Bug: display:block ueberschreibt die
  eingebaute [hidden]-Regel bei gleicher Spezifitaet) -- explizite
  [hidden]-Regel ergaenzt.
- Backend: server-internal/routes/events.js liefert oeffentlich NUR noch
  aktivierte Slots aus (getEventsPublic), neuer authentifizierter Endpunkt
  getEventsAdmin liefert der Verwaltung auch Entwuerfe zum Vorausfuellen.
  10 automatisierte Tests gegen eine Fake-DB bestanden.

Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn
manuell auf dogiintern ausrollen, sonst bleibt die Startseite beim alten
Verhalten (immer beide Slots zeigen, kein Aktiv-Schalter in der Verwaltung).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:43:28 +02:00
DogFatherGitandClaude Opus 5 208b5ba4ed Fix: gemeinsame Navigationsleiste war auf der Verwaltungsseite ~16% groesser
Endlich gefunden (Screenshot-Vergleich Haupt-/Verwaltungsseite Seite an
Seite, exakt gleiche Fensterbreite): verwaltung.html setzt bewusst
`html { font-size: 18.5px }` fuer ihr eigenes "Jewelen-Tresor"-Design
(statt der normalen 16px) -- das ist eine GLOBALE rem-Basis und vergroesserte
dadurch ungewollt auch die gemeinsame, aus main.css/main.js kommende
Navigationsleiste um denselben Faktor (~16%). Dieselbe Leiste brauchte
dadurch spuerbar mehr Breite als auf jeder anderen Seite und lief bei
Fensterbreiten ueber, bei denen sie ueberall sonst laengst gut passte --
kein Cache-Problem, ein echter CSS-Bug, der die ganze vorherige
Fehlersuche erklaert.

Fix: die Navigationsleiste bekommt in verwaltung.html ihre Masse fest in
Pixel zurueck (exakt die Werte, die bei 16px-Basis herauskaemen) statt in
rem -- dadurch bleibt sie unabhaengig von der Basis-Schriftgroesse dieser
Seite exakt so gross wie ueberall sonst. Lokal verifiziert: beide Seiten
liefern jetzt bei identischer Fensterbreite exakt dieselbe Leisten-Breite
(1560px).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:22:45 +02:00
DogFatherGitandClaude Opus 5 fece1dad56 Revert: Hamburger-Schwelle zurueck auf 1650px
Die Erhoehung auf 1900px eben war ein Fehlgriff -- der Nutzer sah dadurch
bei seiner eigentlichen Fensterbreite (die volle Desktop-Nav laengst
gepasst haette, zweifach live bestaetigt) nur noch die schmale Menue-
Ansicht statt der gewohnten vollen Leiste. 1650px war die korrekte,
bereits bestaetigte Schwelle -- das eigentliche Problem war durchgehend
Browser-/CDN-Caching, nicht die Schwelle selbst.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 20:06:18 +02:00
DogFatherGitandClaude Opus 5 0b0e7a2eb9 Nav-Sicherheitsabstand nochmal deutlich vergroessert (Hamburger bis 1900px)
Nutzer meldet weiterhin Ueberlauf trotz zweifach bestaetigtem Live-Test
(exakt seine Fensterbreite 1993x931 gegen den echten Server, 0 Ueberlauf,
mehrfach reproduziert) -- Ursache vermutlich hartnaeckiger lokaler Cache
im jeweiligen Browserprofil, nicht mehr abschliessend ferndiagnostizierbar.
Statt weiter zu diskutieren: Sicherheitsabstand brachial vergroessert,
unabhaengig von der genauen Ursache. Hamburger-Schwelle 1650px -> 1900px --
deckt praktisch jede Laptop-/Desktop-Fensterbreite ab, echte Desktop-Nav
zeigt sich jetzt erst ab sehr breiten Fenstern (>1900px), dort mit viel
Luft (min(1560px, 94vw)).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 20:00:07 +02:00
DogFatherGitandClaude Opus 5 7099d80491 Sichtbarer "App installieren"-Knopf statt versteckter Browser-Funktion
Nutzer-Report: "ich kann sie immer noch nicht runter laden" -- Manifest +
Service Worker reichen technisch fuer Installierbarkeit, aber ohne
sichtbaren Knopf muss man wissen, dass Chrome/Edge das Adressleisten-
Symbol/3-Punkte-Meny dafuer versteckt. Jetzt: echter Knopf im Footer
("Als App installieren", jede Seite) und in der Verwaltung-Session-Leiste
("Als eigene App installieren"), nutzt beforeinstallprompt + prompt() --
loest pro Seite automatisch mit GENAU dem Manifest aus, das diese Seite
selbst verlinkt (index.html -> manifest.json, verwaltung.html ->
manifest-verwaltung.json), kein Sonderfall-Code noetig. Bleibt unsichtbar,
wenn der Browser das nicht unterstuetzt (Safari/iOS) oder die Seite schon
als App laeuft.

Ausserdem: eigener apple-mobile-web-app-title fuer verwaltung.html
("DogiCrew-Verwaltung" statt generisch "DogFather" beim iOS-Home-Bildschirm).

Cache-Busting-Version (?v=) erneut hochgezaehlt (20260820 -> 20260820b),
da main.js sich durch diese Aenderung erneut geaendert hat.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 19:30:42 +02:00
DogFatherGitandClaude Opus 5 4173e43c78 Cache-Busting fuer CSS/JS auf allen Seiten (?v=20260820)
Zusammen mit dem no-cache-Header-Fix in server/index.js: Cloudflare (die
Seite laeuft hinter einem orange-cloud-Proxy) liefert fuer .css/.js einen
eigenen festen Standard-Cache (Browser Cache TTL 4 Std) aus, UNABHAENGIG
vom Origin-Cache-Control -- der no-cache-Header allein reichte deshalb
nicht (per curl bestaetigt: main.css zeigte weiterhin max-age=14400 direkt
nach dem Deploy). Robuste, von Cloudflare-Zoneneinstellungen unabhaengige
Loesung: jede lokale CSS/JS-Referenz auf allen 34 Seiten bekommt einen
Versions-Query-String (?v=20260820) -- fuer Browser/CDN ist das eine neue
URL, alte gecachte Kopien werden dadurch nie mehr faelschlich weiterverwendet.

WICHTIG fuer kuenftige Aenderungen an main.css/main.js: das Datum in ?v=
muss bei der naechsten inhaltlichen Aenderung an einer dieser Dateien
wieder hochgezaehlt werden, sonst greift der Cache-Bust nicht erneut.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 19:26:11 +02:00
DogFatherGitandClaude Opus 5 6dad6a5af5 Fix: Deploys blieben bei Besuchern bis zu 4 Std im Browser-Cache haengen
Nutzer-Report: sah den Nav-Fix und die neue installierbare Verwaltungsseite
trotz erfolgreichem Deploy nicht. Ursache: Express schickt standardmaessig
KEIN Cache-Control mit -- da die Seite hinter Cloudflare (orange-cloud)
liegt, sprang Cloudflare dafuer mit seinem eigenen Standardwert ein
(Browser Cache TTL 4 Std, per curl bestaetigt: max-age=14400). Ein frischer
Deploy war dadurch bis zu 4 Std lang im eigenen Browser-Cache jedes/jeder
Besuchers unsichtbar. Cloudflare respektiert ein vom Origin gesetztes
Cache-Control -- jetzt explizit "no-cache" gesetzt (erzwingt Revalidierung
per ETag bei jedem Laden, kein Performance-Verlust durch schnelle
304-Antworten bei unveraendertem Inhalt).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 19:23:42 +02:00
DogFatherGitandClaude Opus 5 2c5e04e433 Fix: manifest-verwaltung.json wurde von der Zugangsschranke blockiert
Die site-weite Zugangsschranke (server/gate.js) laesst bislang nur den
exakten Pfad /manifest.json unauthentifiziert durch (fuer die
PWA-Installierbarkeit der Haupt-Website noetig) -- das neue
manifest-verwaltung.json fiel dadurch nicht unter die Ausnahme und wurde
zur Login-Seite umgeleitet statt als JSON ausgeliefert zu werden. Neuen
Pfad zur Ausnahmeliste hinzugefuegt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:43:23 +02:00
DogFatherGitandClaude Opus 5 aecf6db509 Verwaltungsseite als eigene, separat installierbare App
Nutzer-Wunsch 20.08.2026: "ich will die Verwaltungsseite auch herunter
laden koennen, so dass ich die originale website und die verwaltungsseite
2 mal getrennt installieren kann, auf dem pc und handy."

- Eigenes manifest-verwaltung.json (eigene "id"/"scope" nur fuer
  verwaltung.html, eigener Name "DogiCrew-Verwaltung", eigenes
  Icon-Set) statt des site-weiten manifest.json (scope "/") -- macht sie
  zu einer technisch eigenstaendigen App-Identitaet, installierbar
  parallel zur Haupt-Website, auf Desktop und Handy.
- Neue Icons: bestehendes Husky-Logo mit Violett/Gold-Verlauf statt
  Babyblau (passend zum "Jewelen-Tresor"-Look der Verwaltungsseite),
  damit beide installierten Apps auch optisch klar unterscheidbar sind.
- Kein zweiter Service Worker noetig -- /sw.js laeuft bereits site-weit
  auf Scope "/" und deckt verwaltung.html automatisch mit ab.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:42:20 +02:00
DogFatherGitandClaude Opus 5 94182cc6ab Fix: Desktop-Nav lief ab ~1500px Fensterbreite ueber den Rand hinaus
Nutzer-Report per Screenshot: "DogiCrew"-Knopf/"BALD"-Badge rechts
abgeschnitten. Ursache: die Hamburger-Schwelle (1480px) und der
Nav-Bar-Breiten-Deckel (1440px) wurden schon zweimal knapp nachgezogen,
reichten aber nicht mehr, seit die Nav um den "DogiCrew"-Knopf + "BALD"-
Badge gewachsen ist (braucht jetzt real ~1491px). Ergebnis: bei JEDER
Fensterbreite ab ca. 1500px (nicht nur in einer schmalen Uebergangszone)
lief die Nav dauerhaft ~50px ueber den Rand.

Diesmal mit echtem Sicherheitsabstand statt wieder nur knapp behoben:
Schwelle 1480px -> 1650px, Deckel 1440px -> 1560px. Per Playwright ueber
den kompletten Bereich 1024-2560px nachgeprueft, keine Ueberlaeufe mehr.
Header ist site-weit eine gemeinsame Vorlage (main.js/main.css), Fix gilt
damit automatisch fuer alle Seiten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:38:46 +02:00
DogFatherGitandClaude Opus 5 bda3198c76 Neue Funktion: Supporter-Abstimmungen (Verwaltung + DogiCrew-Bereich)
Naechste "Ausbaustufe" aus Dogfather_VanVan_Supporter_Abo.odt Abschnitt 20
(siehe Supporter-Abo-System.md), auf Nutzerwunsch "perfektioniere meine
Verwaltungsseite": Dogi/VanVan koennen in verwaltung.html eine Frage mit
2-6 Antwortoptionen auf Deutsch erstellen, automatische Uebersetzung beim
Speichern (gleiches Muster wie "Event des Jahres"). Jede aktive DogiCrew-
Person sieht die Abstimmung in ihrem Supporter-Bereich, stimmt genau einmal
ab (UNIQUE-Constraint in der DB, nicht nur Anwendungslogik), sieht danach
die Live-Ergebnisse. Admin-Seite zeigt Ergebnisbalken live, kann schliessen/
wiedereroeffnen/loeschen.

- Neue Migration 0005_supporter_polls.sql (supporter_polls,
  supporter_poll_votes), neue Berechtigung POLLS_MANAGE.
- server-internal/routes/polls.js: 30 End-to-End-Tests gegen eine
  Fake-DB bestanden (better-sqlite3 laesst sich lokal nicht kompilieren).
- verwaltung.html: neue "Abstimmungen"-Kiste im bestehenden
  vw-overview-box-Stil (violett/pink Ergebnisbalken).
- supporter.html: neue "Aktuelle Abstimmung"-Karte im bestehenden
  Gold-Look, Optionen -> Stimme -> Ergebnisbalken, alle 5 Sprachen.

Backend-Teil (server-internal/, cloudflare-worker/migrations/) noch ohne
Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:07:51 +02:00
DogFatherGitandClaude Opus 5 189b72ec73 Vorschau-Umschalter (Theatermasken-Symbol) vor dem oeffentlichen Start deaktiviert
Nutzer-Wunsch 20.08.2026, vor morgigem oeffentlichem Start: "die leute
sollen das ja nicht selbst auswaehlen koennen". Der Schalter erkannte
Dogi/VanVan bisher nur ueber das rein client-seitig lesbare dogi_role-
Cookie (bewusst NICHT das echte HttpOnly-Sicherheits-Cookie) -- technisch
liesse sich dieses Cookie per Browser-Konsole selbst setzen. Echte Daten
waeren dadurch nie einsehbar (nur eigene Beispieldaten-Vorschau), aber
"soll weg fuer die Oeffentlichkeit" heisst hier bewusst ganz weg, nicht
nur sicherer.

Nur der Aufruf beim Seitenaufbau ist auskommentiert, renderVorschauSchalter()
selbst bleibt unangetastet fuer ein spaeteres internes Testen.

Per Playwright verifiziert: Widget erscheint weder bei normalem Besuch
noch mit manuell gesetztem dogi_role-Cookie.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 15:16:01 +02:00
DogFatherGitandClaude Opus 5 f86b96a22f "Event des Jahres" jetzt in der Verwaltung pflegbar (Bild, Text, Link)
Nutzer-Wunsch 20.08.2026: "will ich in der admin seite das selbst
gestalten können, mit bild und text und am besten auch einen link." Auf
Rueckfrage entschieden: Titel/Text nur auf Deutsch eingeben, die anderen
4 Sprachen werden beim Speichern automatisch uebersetzt (MyMemory, 0€,
kein API-Key) -- bewusste Ausnahme von der sonst geltenden "immer echte
Uebersetzung"-Regel, klar dokumentiert und im Admin-UI selbst als Hinweis
sichtbar. de-CH bekommt denselben deutschen Text (Dialekt ist keine von
Uebersetzungs-APIs unterstuetzte Zielsprache).

Backend (server-internal):
- routes/events.js: getEventsPublic (oeffentlich, keine Session),
  saveEvent + uploadEventImage (beide hinter neuer EVENTS_MANAGE-
  Berechtigung, Owner immer erlaubt). Speichert in der bereits
  bestehenden app_settings-Tabelle (2 feste Slots) statt einer neuen
  Tabelle -- es gibt nie mehr als genau 2 Events.
- lib/translate.js: MyMemory-Anbindung mit "fail closed auf Deutsch"
  pro Sprache, falls der Dienst mal nicht antwortet.
- Bild-Upload per multer, zufaelliger Dateiname (crypto.randomUUID,
  verhindert Path-Traversal ueber den Originalnamen komplett), 5 MB
  Limit, nur jpeg/png/webp, Ablage unter /var/lib/dogfather-internal/
  uploads/events (NICHT im Git-Ordner -- uebersteht Deploys), oeffentlich
  ausgeliefert unter /uploads.
- Neue Berechtigung EVENTS_MANAGE im Katalog (Gruppe "Startseite").

Frontend:
- index.html: laedt /events-of-year beim Aufruf, ueberschreibt pro Slot
  Datum/Titel/Text/Bild/Link NUR wenn dort tatsaechlich etwas gespeichert
  ist -- bleibt der Abruf aus oder ist ein Slot leer, bleibt der bisherige
  fest eingebaute Standardinhalt (dieselben zwei echten Events) stehen.
  Kein Blocker, kein sichtbarer Fehler bei Ausfall.
- verwaltung.html: neue Sektion "🏆 Event des Jahres" (nur mit
  EVENTS_MANAGE sichtbar), 2 Karten mit Bild-Upload+Vorschau, Datum,
  Titel, Text, Link, eigenem Speichern-Knopf pro Event.

Ausfuehrlich getestet, weil server-internal wegen fehlender Visual-
Studio-Build-Tools auf dieser Windows-Maschine nicht lokal mit echtem
better-sqlite3 laufen kann: routes/events.js komplett isoliert gegen eine
Fake-DB getestet (11 Szenarien: oeffentlicher Abruf, fehlende Session,
Session ohne Recht, Owner, Rolle MIT EVENTS_MANAGE, alle Validierungen,
Bild-Upload inkl. falscher Dateityp, Abruf des hochgeladenen Bilds).
index.html per Playwright mit echtem Netzwerk-Mocking gegen zwei
Szenarien getestet (API nicht erreichbar -> Standardinhalt bleibt; API
liefert echte Daten -> nur der befuellte Slot wird ueberschrieben, der
leere bleibt Standard). Dabei einen echten CSS-Bug gefunden und behoben
(display:block auf .jahres-event-bild überschrieb die [hidden]-Regel des
Browsers, leeres Bild waere immer sichtbar gewesen). verwaltung.html per
Playwright mit gemocktem Login+API end-to-end getestet: Formular wird
korrekt vorbefuellt, Bild-Upload + Speichern senden die richtigen Daten.

WICHTIG: server-internal laeuft unter einem eigenen Systembenutzer
(dogiintern), auf den ich (claudian) bewusst KEINEN Zugriff habe -- diese
Aenderung kann ich anders als sonst nicht selbst bis auf den Server
bringen. Dogi muss den Deploy-Schritt fuer server-internal selbst
ausfuehren (git pull + npm install + Neustart des dogiintern-Dienstes).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 15:09:54 +02:00
DogFatherGitandClaude Opus 5 3f3ba835a9 Patricks Zitat ergaenzt
Nutzer-Wunsch 20.08.2026: "Wege entstehen dadurch, dass man sie geht."
(alle 5 Sprachen). Ersetzt "Zitat folgt" auf der Scout-Karte.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 14:28:11 +02:00
DogFatherGitandClaude Opus 5 f8f3e65603 Streamer-gesucht-Seite: neues Hero-Bild (Spicy Media, rot)
Nutzer-Wunsch 20.08.2026: bg-bewerben-creator.jpg + -mobile.jpg (bisher
unscharfes Selfie mit pinker Sonnenbrille) ersetzt durch das vom Nutzer
gelieferte Motiv (Spicy-Media-Look, rot, Silhouette + Logo). Alte Version
lokal gesichert (*.jpg.bak-20-08-2026, nicht eingecheckt).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 14:22:01 +02:00
DogFatherGitandClaude Opus 5 f6cbaec132 Zeitreise: Karte "Das Manager-Team wächst" entfernt
Nutzer-Wunsch 20.08.2026. Karte (Juli 2026) samt zugehoeriger i18n-Keys
(zr_ev8_date/_h3/_p, alle 5 Sprachen) entfernt. 16 -> 15 Karten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 13:16:32 +02:00
DogFatherGitandClaude Opus 5 5c5accd12c Neues Links-Hero-Bild + echtes Community-Foto statt Platzhalter
Nutzer-Wunsch 20.08.2026:
- links.html: neues, vom Nutzer geliefertes "aus dem Universum"-Motiv
  (Weltraum-Szene mit TikTok/Instagram/Snapchat/Discord/Merch-Icons um ein
  "LINKS"-Portal) ersetzt bg-links.jpg + -mobile.jpg. Alte Version lokal
  gesichert (*.jpg.bak-20-08-2026, nicht eingecheckt).
- community.html, Abschnitt "Mehr als Follower": echtes Team-Foto
  (Diene, Ghost, VanVan, Dogi bei einem gemeinsamen Treffen, "TEAM DOGI")
  ersetzt den "Foto folgt"-Platzhalter. Gleiches Muster wie vanvan.html
  (.collage-photo, aspect-ratio:3/4 statt der alten 4/3-Platzhalterbox,
  da das echte Foto Hochformat ist). Jetzt ungenutzte i18n-Keys
  com_bild_platzhalter/com_foto_folgt entfernt.

Per Playwright verifiziert: beide Seiten laden fehlerfrei, keine
fehlgeschlagenen Requests, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:54:53 +02:00
DogFatherGitandClaude Opus 5 071f3d0382 Startseite: neue "Event des Jahres"-Kachel unter den 3 Welt-Kacheln
Nutzer-Wunsch 20.08.2026: volle Breite wie die 3 Welt-Kacheln zusammen,
zeigt immer genau zwei Events. Erste Belegung mit zwei bereits auf
zeitreise.html dokumentierten echten Events (28-Stunden-Stream 7. März
2026, Creator Cup 27. Juli - 2. August 2026) statt Platzhaltertext --
Texte sind gekuerzte, inhaltlich unveraenderte Fassungen der bestehenden
Zeitreise-Beschreibungen, beide Karten verlinken auf zeitreise.html. Alle
5 Sprachen gepflegt. Gleiche Karten-/Tag-Optik wie die Welt-Kacheln, damit
es wie ein natuerlicher vierter Baustein wirkt statt wie ein fremdes
Element.

Per Playwright verifiziert: exakt gleiche Breite wie .hero-grid (1180px),
2 Karten, kein horizontales Scrollen auf Mobil, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:51:17 +02:00
DogFatherGitandClaude Opus 5 9adebd9bc4 KRITISCH: Server-Quellcode + kompletter Git-Verlauf waren oeffentlich abrufbar
Sicherheits-Audit vor dem geplanten oeffentlichen Start morgen (21.08.2026,
Nutzer-Anfrage: "duerfen die leute keinen zugriff auf veraenderungen haben").
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")), express.static
lieferte daher nicht nur die Website aus, sondern auch:
- server/ (inkl. gate.js, das komplette Sicherheitskonzept im Klartext)
- server-internal/ (Admin-/Supporter-Backend-Quellcode)
- cloudflare-worker/ (altes Backend)
- .git/ (VOLLSTAENDIGE Commit-Historie, rekonstruierbar per Git-Dump)
- CLAUDE.md, DEPLOY.md, wrangler.toml, netlify.toml, gate-worker.js,
  gate.html.bak-07-08-2026 (Alt-Backup-Datei einer frueheren Session)

Live nachgewiesen (mit gueltigem Zugangscode -- morgen faellt die Schranke
fuer ALLE weg): /server/gate.js und /.git/config lieferten HTTP 200.
Ursache: serve-static blockt per Default nur Dateien, deren EIGENER Name
mit einem Punkt beginnt (server/.env -> zufaellig schon 404), aber NICHT
rekursiv -- .git/config wird trotzdem ausgeliefert, weil "config" selbst
nicht mit einem Punkt beginnt, nur der Ordner davor.

Fix: eigene Sperr-Middleware VOR express.static, unabhaengig von
gateMiddleware (bleibt also auch nach dem Entfernen der Zugangsschranke
wirksam). Blockt ganze Ordner (server/, server-internal/,
cloudflare-worker/) + versteckte Ordner/Dateien rekursiv (jedes
Pfadsegment, das mit "." beginnt, ausser .well-known) + eine feste Liste
an Alt-Dateien + jedes *.bak-Muster, damit auch kuenftige Backup-Reste
automatisch mitgeschuetzt sind.

Lokal mit echtem Express-Server verifiziert (gateMiddleware absichtlich
deaktiviert, um exakt den morgigen "oeffentlich"-Zustand zu simulieren):
alle vorher gefundenen Luecken jetzt 404, alle echten Seiten/Assets
(index.html, main.css, main.js, manifest.json, robots.txt, favicon)
weiterhin 200.

Getrennt prooft: server-internal/ (eigener Dienst unter
postfach.dogfather-universe.com, Port 4200) hat sein EIGENES,
unabhaengiges Session-System -- jede /admin/*-Route ist einzeln per
requireTeamSession-Middleware abgesichert (in index.js durchgezaehlt,
keine Ausnahme gefunden), live mit einer unauthentifizierten Anfrage
gegen /admin/users/list bestaetigt (401). Dieser Dienst war nie vom
Website-Gate abhaengig und ist von diesem Fund nicht betroffen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:47:36 +02:00
DogFatherGitandClaude Opus 5 1d28d8db2b Kooperation-Seite: Hero-Bild nochmal ausgetauscht (verfeinerte Version)
Nutzer-Wunsch 20.08.2026 (zweite Runde): neues, verfeinertes Motiv (DogFather-
Logo oben, Filipe sitzend mit HasiDog + Husky, "INTERESSE AN EINER
KOOPERATION? JETZT KONTAKT AUFNEHMEN") ersetzt die Version vom selben Tag.
Theme bleibt dogfather/babyblau (passt weiterhin). Alte Version lokal
gesichert (*.jpg.bak-20-08-2026-v2, nicht eingecheckt).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:26:59 +02:00
DogFatherGitandClaude Opus 5 7a69f896bc Dienes Profiltext: Alter durch Geburtsmonat ersetzt
Nutzer-Wunsch 20.08.2026: "Ich bin 37 Jahre alt" -> "Ich bin im August 1988
geboren" (und analog in allen 5 Sprachen), Rest des Satzes (verheiratet,
Mutter von zwei Kindern) unveraendert. Betrifft nur bioHtml.de/de-CH/en/
fr/pt in data-modis.js, keine anderen Vorkommen von "37" im Text.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:06:48 +02:00
DogFatherGitandClaude Opus 5 bfcf35aafa Kooperation-Seite: neues Hero-Bild + Theme Rot -> Blau
Nutzer-Wunsch 20.08.2026: Hintergrundbild von bewerben-kooperation.html
(bg-bewerben.jpg + -mobile.jpg, beide bisher identisch zum alten roten
Spicy-Media-Motiv "WERDE TEIL VON SPICY MEDIA") ersetzt durch das vom
Nutzer geschickte neue Motiv (HasiDog/DogFather/Husky, "EINE KOOPERATION
WOLLEN? DANN HIER MELDEN", bereits babyblau statt rot). Passend dazu
data-theme von "spicymedia" (Rot/Orange) auf "dogfather" (Babyblau/Lila)
umgestellt -- keine hartkodierten Rot-Werte im HTML, daher genuegt die
Theme-Umstellung fuer Knopf-/Link-/Eyebrow-Farben komplett.

Bild aus PNG-Quelle konvertiert (quality=90, optimize+progressive), Seiten-
verhaeltnis/Aufloesung unveraendert uebernommen (kein Hochskalieren). Alte
Bilder lokal gesichert (*.jpg.bak-20-08-2026, nicht eingecheckt).

Per Playwright verifiziert: data-theme=dogfather, --accent=#8fd9ea, keine
fehlgeschlagenen Requests, keine Konsolenfehler, Formular/Footer sehen im
echten (nicht-fullPage-)Screenshot nach dem Scrollen korrekt dunkel aus --
ein zunaechst gefundener "weisser Kasten" war ein reines Playwright-
fullPage-Screenshot-Artefakt (position:fixed-Hintergrund kachelt beim
kuenstlich verlaengerten Full-Page-Screenshot nicht mit), kein echter Bug,
per echtem gescrolltem Viewport-Screenshot gegengeprueft und ausgeschlossen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:04:05 +02:00
DogFatherGitandClaude Opus 5 02f9509042 Zeitreise-Finale-Punkt-Bug + Preis aus dem Abonnieren-Hero-Bild entfernt
1) Nutzer-Report 20.08.2026 ("wieso dieser Punkt da in der Mitte"):
.zeit-node (der leuchtende Zeitleisten-Punkt) ist bei normalen Karten
position:absolute; left:50%; top:50% relativ zu .zeit-ast -- korrekt, weil
die Karte dort nur die halbe Breite einnimmt. Die Finale-Karte
(.zeit-final) wird aber auf fast volle Breite gestreckt und zentriert,
der Knoten landete dadurch mitten im Zitat-Text. Die Verbindungslinie
(::before) wurde dafuer schon frueher ausgeblendet, der Knoten selbst
wurde dabei uebersehen -- jetzt nachgezogen (display:none fuer
.zeit-final .zeit-node).

2) Nutzer-Wunsch 20.08.2026: der Preis "FÜR 4,99 € MONATLICH" stand fest
ins Hero-Bild von abonnieren.html eingebrannt (bg-abonnieren.jpg +
-mobile.jpg, beide Dateien waren identisch) -- per CSS/Text nicht
erreichbar, siehe bereits dokumentierter Fund vom selben Tag. Per Pillow
sauber herausretuschiert (Clone-Stamp aus einem textfreien Bereich
derselben Schaltflaeche, exakt auf die Zeilenhoehe inkl. Ü-Umlautpunkte
skaliert, Nahtstellen weich gezeichnet, mit numpy-Helligkeitsanalyse
zeilenweise gegengeprueft bis keine Text-Reste mehr uebrig waren) --
"ABONNIEREN" bleibt als eigenstaendiger Button stehen, keine sichtbare
Lücke/Leerstelle. Qualitaet/Dateigroesse an das Original angepasst
(quality=90, optimize+progressive, exakt vergleichbare Groesse). Original
lokal gesichert (bg-abonnieren.jpg.bak-20-08-2026, nicht eingecheckt).

Beide Fixes per Playwright verifiziert: Knoten-Punkt display:none bei der
Finale-Karte, Hero-Bild laedt fehlerfrei ohne fehlgeschlagene Requests,
keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:49:26 +02:00
DogFatherGitandClaude Opus 5 7e369c72ae Zeitreise: echte Daten fuer "Team Dogi entsteht" + "geht online"
Nutzer-Wunsch 20.08.2026:
- "Team Dogi entsteht": "Datum folgt" -> "Anfang 2025" (neuer Key
  zr_ev2_date, alle 5 Sprachen). Bleibt an ihrer Stelle -- liegt weiterhin
  chronologisch vor "Ostern 2025".
- "DOGFATHER UNIVERSE geht online": "2026 - Datum folgt" -> "21. August
  2026, 21 Uhr" (morgen, live). Bleibt ebenfalls an ihrer Stelle.

Per Playwright komplette Reihenfolge erneut gegengeprueft: weiterhin
chronologisch korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:36:47 +02:00
DogFatherGitandClaude Opus 5 895f04c524 Zeitreise: echtes Datum fuer "Start als Content Creator" (24.10.2023)
Nutzer-Wunsch 20.08.2026. Neuer i18n-Key zr_ev1_date (alle 5 Sprachen),
Karte bleibt weiterhin an erster Stelle, da 2023 das fruehste Datum der
gesamten Zeitleiste ist. Per Playwright die komplette Reihenfolge erneut
durchgezaehlt: weiterhin korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:33:55 +02:00
DogFatherGitandClaude Opus 5 f750fe9dfd Zeitreise: echte Daten fuer Buch + Spotify, chronologisch einsortiert
Nutzer-Wunsch 20.08.2026: "Das Buch HasiDog & Casper" (November 2025) und
"HasiDog startet auf Spotify" (1. Januar 2026) hatten bisher "Datum folgt"
und lagen dadurch weit hinten in der Zeitleiste, obwohl beide zeitlich vor
dem 28-Stunden-Stream (7. März 2026) liegen. Neue i18n-Keys zr_ev13_date/
zr_ev14_date (alle 5 Sprachen) ergaenzt, beide Karten an die chronologisch
richtige Stelle verschoben: Ostern 2025 -> November 2025 (Buch) ->
1. Januar 2026 (Spotify) -> 7. März 2026 (28-Stunden-Stream) -> ...

"Kooperation mit VanVan Teddys" bleibt bewusst unveraendert bei "Datum
folgt", da dafuer noch kein Datum genannt wurde.

Per Playwright die komplette Reihenfolge alle 16 Karten durchgezaehlt und
gegengeprueft: chronologisch korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:26:19 +02:00
DogFatherGitandClaude Opus 5 9cb4e177ee Zeitreise: 4 Ereignisse entfernt + einladender "wächst noch"-Hinweis
Nutzer-Wunsch 20.08.2026: vier Zeitleisten-Karten sollten komplett weg
("Ein neues Kapitel beginnt" / Mai 2026, "Die ersten betreuten Creator",
"Weitere Scouts und Manager kommen hinzu", "Das erste Community-Treffen") --
entfernt aus zeitreise.html samt der zugehoerigen, jetzt ungenutzten
i18n-Keys (zr_ev5_*, zr_ev16_*, zr_ev17_*, zr_ev18_*). 20 -> 16 Karten.

Zusaetzlich: gleich am Seitenanfang soll auffallen, dass diese Zeitreise
noch nicht fertig ist. Bewusst NICHT die alte .todo-note-Optik reaktiviert
(die war fuer Entwickler gedacht und wurde am 31.07.2026 sitewide bewusst
unsichtbar gemacht) -- stattdessen ein neuer, fuer Besucher gestalteter
Hinweis (.zeit-baustelle), der zum Wachstums-Baum-Thema der Seite passt
(🌱 "Der Anfang" oben, 🌳 "...wächst weiter" unten): "Diese Zeitreise
wächst noch", babyblauer Glow-Rahmen, leicht wiegendes Setzlings-Icon.
Alle 5 Sprachen gepflegt.

Per Playwright verifiziert: alle 4 Karten wirklich weg (Text-Suche),
16 statt 20 .zeit-ast-Elemente, Hinweisbox sichtbar mit korrektem Text,
keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:23:38 +02:00
DogFatherGitandClaude Opus 5 c4fa132244 DogiCrew-Sperre: auch die Registrierungsfelder selbst nicht mehr eintippbar
Nutzer-Feedback 20.08.2026 (direkte Nachpruefung): "ich kann immer nur was
rein tippen, soll garnicht möglich sein bitte" -- der Registrieren-Knopf
war gesperrt, aber Name/TikTok/E-Mail-Feld ließen sich weiterhin normal
beschreiben. Jetzt disabled, exakt wie das E-Mail-Feld im Login-Panel
(voriger Commit). Per Playwright verifiziert: echter Tippversuch in allen
drei Feldern hinterlässt keinen Wert mehr.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:10:26 +02:00
DogFatherGitandClaude Opus 5 d98133faba DogiCrew-Sperre vervollstaendigt: Login-Einstieg, E-Mail-Feld, Preis
Nutzer-Feedback 20.08.2026 (Nachpruefung des vorherigen Commits): der
Registrieren-Knopf war gesperrt, aber drei Luecken blieben offen --

1. "Schon Supporter? Hier einloggen" fuehrte zu einem VOLL FUNKTIONSFAEHIGEN
   Login-Code-Anfordern-Knopf. E-Mail-Feld jetzt disabled (nicht eintippbar,
   wie gewuenscht) und btn-login-request auf denselben .btn-gold-locked-Stil
   wie der Registrieren-Knopf umgestellt (natives disabled-Attribut).
2. Dritte, bislang uebersehene Luecke: "Mit Google anmelden" waere ein
   weiterer Weg gewesen, trotz gesperrter Knoepfe ein echtes Konto anzulegen,
   falls GOOGLE_CLIENT_ID serverseitig konfiguriert ist. initGoogleConsent()
   wird jetzt nicht mehr aufgerufen, solange die Registrierung gesperrt ist --
   ein Kommentar markiert genau die Stelle zum spaeteren Reaktivieren.
3. Der Preis (4,99 €) durfte laut Nutzer noch nicht sichtbar sein -- war aber
   an zwei Stellen zu sehen: der grossen Preis-Zahl in der Box (jetzt
   "Coming soon…" im selben Gold-Schimmer-Stil wie der gesperrte Knopf) und
   im Fliesstext (ab_text2, alle 5 Sprachen: "Für 4,99 € im Monat" ->
   "Mit deinem/dim/your/ta/sua monatlichen Beitrag", nur die erste Teilphrase
   geaendert, Rest jeder Uebersetzung unangetastet).

Betrifft ausschliesslich den oeffentlichen Anmelde-Einstieg -- bereits
eingeloggte Supporter (Dogi/VanVan als Test-Accounts) sind ueber ihren
gespeicherten Token/supporter.html unveraendert erreichbar.

Bekannter, NICHT in diesem Commit geloester Rest: der grosse Hero-Banner
oben auf der Seite (assets/img/bg-abonnieren(-mobile).jpg) zeigt den Preis
ebenfalls fest ins Bild eingebrannt ("ABONNIEREN FÜR 4,99 € MONATLICH") --
das ist Bildmaterial, keine Text-/CSS-Aenderung, braucht Ruecksprache mit
Dogi bevor daran gearbeitet wird.

Vor dem Commit per Playwright verifiziert: Preis-Box zeigt nur noch
"Coming soon…", Login-E-Mail-Feld nimmt keine Eingabe an, Klick auf den
gesperrten Login-Knopf loest keinen /supporter/login-request-Aufruf aus,
Google-Bereich bleibt hidden, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:07:24 +02:00
DogFatherGitandClaude Opus 5 5cd473e71a DogiCrew-Registrierung: "Coming Soon"-Sperre + Bald-Badge im Nav-Knopf
Nutzer-Wunsch 20.08.2026: DogiCrew ist "das geilste Bonus" -- der Registrieren-
Knopf soll noch nicht bedienbar sein (Dogis PayPal-Business-Zugangsdaten fehlen
noch, siehe DogFather Website - Offene Punkte.md), aber Besucher sollen ueber
den Nav-Knopf trotzdem ganz normal auf die Seite kommen und die Vorschau sehen
duerfen -- "soll alles bleiben" ausser dem einen Knopf.

- Nav-Knopf (DogiCrew/Abo aktivieren, main.js renderAboButton): neues kleines
  Schimmer-Badge "Bald" direkt in der Pille (main.css .nav-cta-soon, reine
  Wiederverwendung von .btn-silver-text/-spark in kleinerem Massstab). Nur auf
  den drei Zielen, die zur (noch gesperrten) Registrierung fuehren -- NICHT auf
  supporter.html, das ist der echte, bereits funktionierende Bereich fuer
  Dogi/VanVan als Test-Supporter.
- abonnieren.html: Registrieren-Knopf ersetzt durch neue .btn-gold-locked-
  Komponente -- eigener Gold/Schloss-Stil (nicht das schon anderswo auf dieser
  Seite vergebene .btn-silver), natives disabled-Attribut (kein JS noetig,
  disabled-Buttons feuern keine Click-Events -- der bestehende Listener bleibt
  unveraendert und inert). Formularfelder bleiben normal ausfuellbar (Teaser),
  nur der Absende-Knopf ist gesperrt.

Bug waehrend der Umsetzung gefunden UND behoben, nicht nur uebersehen: die
rotierende Conic-Gradient-Randmaske von .btn-silver/.btn-legendary (copy-paste
als erster Versuch) verzieht sich auf diesem sehr langgestreckten 100%-Breite-
Knopf zu einer krummen Schlaufe -- exakt die dokumentierte Lektion in
Projektregeln.md Punkt 14 (03.08.2026, TikTok-Button-Bug), die beim ersten
Entwurf übersehen wurde. Per Playwright-Screenshots über mehrere Animations-
Frames nachgewiesen (nicht nur vermutet) und auch am bereits LIVE laufenden
.btn-silver auf dieser Seite reproduziert, um auszuschliessen, dass es an der
neuen Komponente statt an der Technik selbst liegt. Fix: derselbe sichere
Zwei-Layer-Background-Trick wie bei .abo-google-frame (gleiche Datei) --
Bewegung nur ueber background-position, bleibt geometrieunabhaengig exakt an
der Kontur. Nach dem Fix erneut ueber mehrere Frames verifiziert, sauber.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 10:51:34 +02:00
DogFatherGitandClaude Opus 5 0223fcf9cd Mobil-Menü: schwebende Eck-Knöpfe blockierten Sprachschalter + Bewerben-Button
Nutzer-Report 20.08.2026: "auf dem Handy... passt nicht immer alles". Per echter
Mobil-Emulation (iPhone 15 Pro + Pixel 8, Playwright) nachgestellt statt geraten:
Die schwebenden Eck-Knöpfe (Musik-Widget unten rechts, Vorschau-Umschalter fuer
Dogi/VanVan unten links) sind absichtlich immer sichtbar (position:fixed, direkte
<body>-Kinder). Beim aufgeklappten Mobil-Hamburger-Menü lagen sie dadurch sichtbar
UEBER den untersten Menuepunkten (Sprachschalter "DE" + "Bewerben"-Knopf) und
verdeckten sie -- auf Screenshots klar zu sehen, betrifft jede der ueber 30 Seiten,
da das Menü ueberall gleich ist. Genau die Art Bug, die im Browser (Adressleiste
noch da, man kann drumrum navigieren) kaum auffaellt, aber in der installierten
Vollbild-App voll durchschlaegt.

Fix: :has()-Selektor blendet beide Widgets aus, solange .nav-links.open ist --
kein gemeinsamer Elternknoten mit dem Menue vorhanden, daher CSS statt weiterer
main.js-Logik. Nach dem Fix per Regressionstest ueber 6 Seiten x 2 Geraete erneut
verifiziert: Widgets erscheinen normal wieder, sobald das Menue schliesst, keine
neuen Konsolenfehler, kein horizontales Scrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 10:28:22 +02:00
92 changed files with 5131 additions and 581 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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/i18n-404.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+67 -16
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<style> <style>
/* ===================================================================== /* =====================================================================
Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback
@@ -265,7 +265,12 @@
<div class="abo-logo-glut" aria-hidden="true"></div> <div class="abo-logo-glut" aria-hidden="true"></div>
<div class="abo-logo-wz" aria-hidden="true"></div> <div class="abo-logo-wz" aria-hidden="true"></div>
<span class="eyebrow" data-i18n="ab_preis_eyebrow">DogiCrew 🩵</span> <span class="eyebrow" data-i18n="ab_preis_eyebrow">DogiCrew 🩵</span>
<div class="abo-preis-zahl">4,99 € <small data-i18n="ab_preis_pro_monat">/ Monat</small></div> <!-- Bugfix/Nutzer-Wunsch 20.08.2026: Preis noch NICHT zeigen, solange die
Registrierung gesperrt ist (siehe .btn-gold-locked weiter unten) — sonst
wäre der Preis schon öffentlich, bevor überhaupt jemand abschließen kann.
Gleicher Chrome-Gold-Schimmer-Text wie beim gesperrten Knopf
(.btn-gold-locked-text), hier freistehend statt in einem Knopf. -->
<div class="abo-preis-zahl"><span class="btn-gold-locked-text" style="font-size:1em;" data-i18n="ab_kollektion_coming_soon">Coming soon…</span></div>
<p class="small muted" style="margin-top:-.6rem;margin-bottom:1rem;">Keine gesondert ausgewiesene MwSt. gemäß Kleinunternehmerregelung (Art. 57 des luxemburgischen TVA-Gesetzes) — keine weiteren Kosten.</p> <p class="small muted" style="margin-top:-.6rem;margin-bottom:1rem;">Keine gesondert ausgewiesene MwSt. gemäß Kleinunternehmerregelung (Art. 57 des luxemburgischen TVA-Gesetzes) — keine weiteren Kosten.</p>
<ul class="abo-bedingungen"> <ul class="abo-bedingungen">
<li data-i18n="ab_bed1">Monatliche automatische Zahlung über PayPal</li> <li data-i18n="ab_bed1">Monatliche automatische Zahlung über PayPal</li>
@@ -308,30 +313,65 @@
<!-- Zustand 2: Registrierung --> <!-- Zustand 2: Registrierung -->
<div class="abo-panel" id="panel-register" hidden> <div class="abo-panel" id="panel-register" hidden>
<!-- Bugfix/Nutzer-Wunsch 20.08.2026 (Nachpruefung): Nur der Knopf war
gesperrt, die drei Felder ließen sich trotzdem antippen und
beschreiben ("ich kann immer nur was rein tippen, soll garnicht
möglich sein"). Jetzt wie das E-Mail-Feld im Login-Panel: disabled
statt nur readonly, damit auch Tastatur-/Screenreader-Nutzung
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" /> <input type="text" id="reg-name" maxlength="60" disabled />
</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" /> <input type="text" id="reg-tiktok" placeholder="@benutzername" maxlength="40" disabled />
</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" /> <input type="email" id="reg-email" maxlength="120" disabled />
</div> </div>
<button type="button" class="btn btn-primary" style="width:100%;" id="btn-register" data-i18n="ab_btn_register">Registrieren</button> <!-- 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> <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: Login (E-Mail-Code anfordern) -->
<!-- Bugfix/Nutzer-Wunsch 20.08.2026: War die einzige noch offene Lücke, nachdem
der Registrieren-Knopf gesperrt wurde — über "Schon Supporter? Hier
einloggen" kam man hierher und konnte trotzdem einen echten Login-Code
anfordern, obwohl die DogiCrew noch gar nicht live ist. Jetzt genauso
gesperrt: E-Mail-Feld disabled (nicht eintippbar, wie gewünscht), Knopf
derselbe .btn-gold-locked-Stil wie beim Registrieren-Knopf. Betrifft NUR
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" /> <input type="email" id="login-email" maxlength="120" disabled />
</div> </div>
<button type="button" class="btn btn-primary" style="width:100%;" id="btn-login-request" data-i18n="ab_btn_login_code">Login-Code anfordern</button> <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="switch-to-register" data-i18n="ab_switch_register">Noch kein Konto? Jetzt registrieren</a></p>
</div> </div>
@@ -387,7 +427,10 @@
<div class="container grid grid-2"> <div class="container grid grid-2">
<div class="card"> <div class="card">
<p data-i18n="ab_text1">Dieses Abonnement ist kein gewöhnlicher Shop und kein einfacher Kauf. Es ist für die Menschen gedacht, die Dogfather dauerhaft unterstützen und ein besonderer Teil dieser gemeinsamen Reise sein möchten.</p> <p data-i18n="ab_text1">Dieses Abonnement ist kein gewöhnlicher Shop und kein einfacher Kauf. Es ist für die Menschen gedacht, die Dogfather dauerhaft unterstützen und ein besonderer Teil dieser gemeinsamen Reise sein möchten.</p>
<p data-i18n="ab_text2">Für 4,99 € im Monat erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.</p> <!-- Bugfix/Nutzer-Wunsch 20.08.2026: "4,99 €" hier entfernt (nur die Zahl, Rest
des Satzes unverändert) — sonst hätte der eigentlich gesperrte Preis
weiter oben in der Preis-Box hier im Fließtext trotzdem gestanden. -->
<p data-i18n="ab_text2">Mit deinem monatlichen Beitrag erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.</p>
<ul class="abo-vorteile"> <ul class="abo-vorteile">
<li data-i18n="ab_vorteil1">Exklusiver Supporter-Bereich</li> <li data-i18n="ab_vorteil1">Exklusiver Supporter-Bereich</li>
<li data-i18n="ab_vorteil2">Kleiner Hasen-Teddy nach 6 bezahlten Monaten</li> <li data-i18n="ab_vorteil2">Kleiner Hasen-Teddy nach 6 bezahlten Monaten</li>
@@ -411,9 +454,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-abonnieren.js"></script> <script src="assets/js/i18n-abonnieren.js?v=20260821n"></script>
<script src="assets/js/supporter.js"></script> <script src="assets/js/supporter.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
(function () { (function () {
const S = window.DogiSupporter; const S = window.DogiSupporter;
@@ -662,7 +705,15 @@
} 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. */
} }
initGoogleConsent(); /* Bugfix/Nutzer-Wunsch 20.08.2026: "Mit Google anmelden" wäre ein dritter, bisher
übersehener Weg gewesen, trotz gesperrtem Registrieren-/Login-Knopf doch ein
echtes Konto anzulegen bzw. sich einzuloggen (falls GOOGLE_CLIENT_ID auf dem
Server konfiguriert ist). initGoogleConsent() daher bewusst NICHT mehr
aufgerufen, solange die DogiCrew-Registrierung gesperrt ist — panel-social-login
bleibt dadurch dauerhaft hidden. Einfach diesen Aufruf wieder aktivieren,
sobald die Registrierung selbst (btn-register/btn-login-request, siehe oben)
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/main.js?v=20260821n"></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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/data-aktionen.js?v=20260821n"></script>
<script src="assets/js/i18n-aktion-detail.js"></script> <script src="assets/js/i18n-aktion-detail.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
+802 -26
View File
@@ -121,17 +121,14 @@ header.site-header {
braucht durch größere Schrift + mehr Abstand + Polarlicht-Padding braucht durch größere Schrift + mehr Abstand + Polarlicht-Padding
spürbar mehr Platz, ohne dass der restliche Seiteninhalt breiter spürbar mehr Platz, ohne dass der restliche Seiteninhalt breiter
werden soll. werden soll.
Bugfix 04.08.2026: Der feste 1400px-Deckel war zu knapp — der Bugfix 04.08.2026: 1400px -> 1440px angehoben (siehe damalige Analyse).
tatsächliche Inhalt (Logo+Menü+Sprachschalter+DogiCrew-Knopf+ Bugfix 20.08.2026: reichte seither wieder nicht mehr — die Nav braucht
Bewerben-Knopf) braucht bei aufgeklapptem Desktop-Menü (ab 1480px inzwischen (Nav-Punkt "DogiCrew" + "BALD"-Badge dazugekommen) real ca.
Breite, siehe Media Query unten) ca. 1417px und lief dadurch IMMER 1491px, der Deckel blieb bei 1440px. 1440px -> 1560px, zusammen mit der
um ca. 17px über den Rahmen hinaus — auf breiten Bildschirmen ohne auf 1650px angehobenen Hamburger-Schwelle weiter unten (siehe Kommentar
sichtbaren Schaden (genug Luft drumherum), aber bei schmaleren dort) — diesmal mit echtem Sicherheitsabstand statt wieder nur knapp
Fenstern knapp über der 1480px-Schwelle (z.B. Laptop-Fenster nicht genug für den aktuellen Stand. */
maximiert, oder Browser-Zoom >100%) wurde dadurch der rechte Rand width: min(1560px, 94vw);
(der "Bewerben"-Knopf) sichtbar abgeschnitten. 1400px -> 1440px gibt
ausreichend Puffer, ohne auf breiten Screens spürbar größer zu wirken. */
width: min(1440px, 94vw);
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -414,11 +411,21 @@ header.site-header {
0% { background-position: 160% 0; } 0% { background-position: 160% 0; }
100% { background-position: -60% 0; } 100% { background-position: -60% 0; }
} }
/* Bugfix 20.08.2026 (Nutzer-Report per Screenshot-Vergleich: Kategorie-
Menüs wie "Medien" sind mittig unter dem Knopf, der Sprachschalter aber
rechtsbündig) — war bewusst rechtsbündig, seit der Sprachschalter aber
nicht mehr ganz am rechten Rand sitzt (DogiCrew-/Bewerben-Knopf daneben),
ist genug Platz für dieselbe zentrierte Ausrichtung wie überall sonst.
left/transform hier deshalb NICHT mehr überschrieben (erbt jetzt
left:50%/translateX(-50%) von der allgemeinen .nav-dropdown-Regel,
genau wie der bereits zentrierte Verbindungs-Pfeil (::before) es die
ganze Zeit schon war — vorher zeigte der Pfeil mittig, während die Box
selbst daneben saß). Eigene Animation bleibt bestehen (siehe Kommentar
der allgemeinen .nav-dropdown-Animation), jetzt aber ebenfalls mit
translateX(-50%) in jedem Frame, sonst würde sie die Zentrierung wieder
aufheben. */
.sprach-schalter .nav-dropdown { .sprach-schalter .nav-dropdown {
min-width: 190px; min-width: 190px;
left: auto;
right: 0;
transform: none;
padding: .4rem; padding: .4rem;
border-radius: 14px; border-radius: 14px;
border: 1px solid rgba(210, 216, 224, .55); border: 1px solid rgba(210, 216, 224, .55);
@@ -427,12 +434,11 @@ header.site-header {
0 14px 34px rgba(0, 0, 0, .6), 0 14px 34px rgba(0, 0, 0, .6),
0 0 16px rgba(220, 226, 235, .35), 0 0 16px rgba(220, 226, 235, .35),
0 0 34px rgba(190, 200, 215, .18); 0 0 34px rgba(190, 200, 215, .18);
transform-origin: top right;
animation: sprach-dropdown-in .18s ease both, sprach-silber-puls 3.6s ease-in-out infinite; animation: sprach-dropdown-in .18s ease both, sprach-silber-puls 3.6s ease-in-out infinite;
} }
@keyframes sprach-dropdown-in { @keyframes sprach-dropdown-in {
from { opacity: 0; transform: translateY(-6px) scale(.96); } from { opacity: 0; transform: translateX(-50%) translateY(-6px) scale(.96); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
} }
/* Silberner Polarlicht-Schein um den Dropdown-Rand — bewusst neutral /* Silberner Polarlicht-Schein um den Dropdown-Rand — bewusst neutral
silber statt farbig, damit sich der schwarze Kasten von der bunten silber statt farbig, damit sich der schwarze Kasten von der bunten
@@ -613,6 +619,34 @@ header.site-header {
0 0 22px rgba(143, 217, 234, .35), 0 0 22px rgba(143, 217, 234, .35),
0 0 22px rgba(139, 92, 246, .3); 0 0 22px rgba(139, 92, 246, .3);
} }
/* "Bald"-Badge auf dem Abo-Knopf, solange die Registrierung auf
abonnieren.html gesperrt ist (20.08.2026, siehe .btn-gold-locked weiter
unten + main.js renderAboButton/soonBadge). Sitzt als zweites Kind IN
der Pille (nicht als eigenes Nav-Element), bleibt dadurch Teil desselben
Klicks — der Knopf führt bewusst weiterhin ganz normal zu abonnieren.html,
nur die Registrierung selbst dort ist gesperrt. Bewusst neutral-silbern
(Wiederverwendung von .btn-silver-text/-spark, nur kleiner skaliert)
statt Gold, damit es nicht mit der babyblau/lila-Aurora dieser Pille
konkurriert — reine Status-Markierung, kein zweiter Marken-Akzent. */
.nav-cta-soon {
position: relative;
z-index: 2;
display: inline-flex;
align-items: center;
gap: .22rem;
margin-left: .5rem;
padding: .15rem .5rem;
border-radius: 999px;
font-size: .64rem;
font-weight: 800;
letter-spacing: .04em;
text-transform: uppercase;
background: rgba(255, 255, 255, .07);
border: 1px solid rgba(255, 255, 255, .3);
box-shadow: 0 0 10px rgba(230, 235, 242, .3);
flex-shrink: 0;
}
.nav-cta-soon .btn-silver-spark { font-size: .85em; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.nav-cta::before, .nav-cta::after { animation: none; } .nav-cta::before, .nav-cta::after { animation: none; }
} }
@@ -937,6 +971,108 @@ header.site-header {
} }
} }
/* =====================================================================
"Gesperrter Tresor"-Knopf für den DogiCrew-Registrieren-Knopf
(abonnieren.html, 20.08.2026, Nutzer-Wunsch: "richtig krank übertrieben
geiles... weil es wird ja das geilste Bonus und überhaupt so von mir").
Bewusst KEINE Wiederverwendung von .btn-silver — der ist neutral-grau und
auf DIESER Seite bereits für die Teddy-Kollektion (Nebensache) vergeben.
Chrome-Text-Clip-Schimmer + Schimmer-Sweep + Halo-Puls wie .btn-silver/
.btn-legendary, aber in den ECHTEN Gold-Tönen dieser Seite
(#ffe9a8/#d4af37, siehe .abo-panel .btn) und mit einem pulsierenden
Schloss statt Sternen, damit die Haupt-CTA sichtbar "das Besondere,
noch verschlossene" bleibt statt nur "irgendwann verfügbar".
WICHTIG: Für den Rand bewusst NICHT die rotierende Conic-Gradient-Maske
von .btn-silver/.btn-legendary kopiert — dieser Knopf läuft über
`width:100%` auf voller Kartenbreite (~440px) bei nur ~62px Höhe, also
ein sehr langgestrecktes Rechteck. Genau für diesen Fall ist die
dokumentierte Lektion in Projektregeln.md Punkt 14 gedacht (03.08.2026,
"TikTok-Button-Bug"): ein rotierendes maskiertes Pseudo-Element verzieht
sich auf länglichen Formen zu einer krummen Schlaufe, weil ein Rechteck
beim Rotieren nicht rotationssymmetrisch ist — live mit Playwright-
Screenshots über mehrere Frames nachgewiesen (die Schlaufe ist der
rotierende Rand bei verschiedenen Winkeln). Stattdessen derselbe sichere
Zwei-Layer-Background-Trick wie bei .abo-google-frame weiter oben in
dieser Datei (Füllung padding-box + Verlauf border-box, Bewegung nur
über background-position) — bleibt geometrisch exakt an der Kontur,
unabhängig vom Seitenverhältnis. Reine Deko/Statusanzeige — echte
Deaktivierung läuft über das native disabled-Attribut am <button>
(siehe abonnieren.html), nicht über pointer-events, damit auch
Tastatur-/Screenreader-Nutzung korrekt "nicht bedienbar" meldet. Bewusst
KEIN :hover-Zustand (keine Lift-Animation wie bei .btn-silver/
.btn-legendary) — soll nicht so wirken, als wäre er doch klickbar. */
.btn-gold-locked {
position: relative;
isolation: isolate;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
gap: .65rem;
padding: 1rem 1.4rem;
border-radius: 14px;
font-weight: 800;
font-size: 1.04rem;
letter-spacing: .02em;
border: 1.5px solid transparent;
background:
linear-gradient(160deg, #1c1710, #0b0e14 55%, #211a0d) padding-box,
linear-gradient(120deg, #d4af37, #ffe9a8, #b8912b, #fff2c9, #d4af37) border-box;
background-size: 100% 100%, 260% 100%;
cursor: not-allowed;
animation: btn-gold-locked-border-shine 6s ease-in-out infinite, btn-gold-locked-halo 3s ease-in-out infinite;
box-shadow: 0 8px 22px rgba(0, 0, 0, .45), 0 0 0 1px rgba(212, 175, 55, .08) inset;
}
.btn-gold-locked-lock {
position: relative;
z-index: 2;
display: inline-block;
font-size: 1.05em;
filter: drop-shadow(0 0 6px rgba(255, 214, 102, .85));
animation: btn-gold-locked-pulse 2.2s ease-in-out infinite;
}
.btn-gold-locked-text {
position: relative;
z-index: 2;
background: linear-gradient(100deg, #ffe9a8 0%, #ffffff 24%, #d4af37 42%, #ffe9a8 60%, #ffffff 78%, #d4af37 100%);
background-size: 260% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6));
animation: btn-silver-text-shine 3.6s linear infinite; /* bestehendes Keyframe wiederverwendet */
}
.btn-gold-locked::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
background: linear-gradient(115deg, transparent 32%, rgba(255, 233, 168, .5) 49%, transparent 66%);
background-size: 240% 100%;
mix-blend-mode: overlay;
animation: nav-cta-shimmer 4s linear infinite; /* bestehendes Keyframe wiederverwendet */
pointer-events: none;
}
@keyframes btn-gold-locked-border-shine {
0%, 100% { background-position: 0 0, 0% 50%; }
50% { background-position: 0 0, 100% 50%; }
}
@keyframes btn-gold-locked-pulse {
0%, 100% { opacity: .7; transform: scale(.92) rotate(-4deg); }
50% { opacity: 1; transform: scale(1.12) rotate(4deg); }
}
@keyframes btn-gold-locked-halo {
0%, 100% { box-shadow: 0 8px 22px rgba(0, 0, 0, .45), 0 0 16px rgba(212, 175, 55, .22); }
50% { box-shadow: 0 8px 22px rgba(0, 0, 0, .45), 0 0 32px rgba(255, 214, 102, .45); }
}
@media (prefers-reduced-motion: reduce) {
.btn-gold-locked, .btn-gold-locked::after,
.btn-gold-locked-text, .btn-gold-locked-lock {
animation: none;
}
}
/* ===================================================================== /* =====================================================================
.btn-holo — zweiter "Mega"-Button-Stil, bewusst MECHANISCH anders als .btn-holo — zweiter "Mega"-Button-Stil, bewusst MECHANISCH anders als
.btn-legendary/.btn-silver/.btn-tiktok (die alle nach demselben Muster .btn-legendary/.btn-silver/.btn-tiktok (die alle nach demselben Muster
@@ -1308,14 +1444,21 @@ header.site-header {
cursor: pointer; cursor: pointer;
} }
/* Schwelle von ursprünglich 880px auf 1480px angehoben: nach mehreren /* Bugfix 20.08.2026 (Nutzer-Report per Screenshot: "DogiCrew"-Knopf/Badge
Vergrößerungsrunden (Logo/Nav-Schrift/Sprachschalter/Bewerben-Button, rechts abgeschnitten): die Schwelle war schon zweimal angehoben (880 ->
mehr Abstand zwischen den Nav-Punkten, größerer eigener Nav-Container, 1480px, dazu der .nav-bar-Deckel 1400 -> 1440px), reichte aber seither
Polarlicht-Padding je Kategorie) braucht die Desktop-Nav deutlich mehr NICHT mehr aus — seit den neu dazugekommenen Nav-Punkten "DogiCrew" +
Platz als früher. Live bei mehreren Breiten nachgeprüft (siehe Commits "BALD"-Badge braucht die aufgeklappte Desktop-Nav real ca. 1491px
vom 02.08.2026) — Schwelle hier bewusst großzügig, damit nie wieder Breite (per Playwright über mehrere Breiten nachgemessen), der Deckel
Elemente sichtbar aus der Leiste verschwinden. */ blieb aber bei 1440px hart begrenzt. Ergebnis: bei JEDER Fensterbreite
@media (max-width: 1480px) { ab ca. 1500px (nicht nur in einer schmalen Übergangszone) lief die Nav
permanent ca. 50px über den Rand hinaus. Diesmal mit echtem Sicherheits-
abstand behoben statt wieder nur knapp: Schwelle auf 1650px, Deckel auf
1560px — bei exakt 1650px ergibt min(1560px, 94vw) ≈ 1551px, spürbar
mehr als die gemessenen ~1491px nötigen Breite. Bei jeder Fensterbreite
NACH der Schwelle reicht 94vw automatisch weiter aus (nur ganz nah an
der Schwelle war die Lücke kritisch). */
@media (max-width: 1650px) {
.nav-toggle { display: block; } .nav-toggle { display: block; }
.nav-links { .nav-links {
position: fixed; position: fixed;
@@ -1336,6 +1479,22 @@ header.site-header {
.nav-dropdown::before { display: none; } .nav-dropdown::before { display: none; }
.nav-group.open .nav-dropdown { display: flex; } .nav-group.open .nav-dropdown { display: flex; }
.nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; } .nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
/* Bugfix 20.08.2026 (Nutzer-Report: "auf dem Handy passt nicht immer alles").
Per echter Mobil-Emulation (iPhone 15 Pro + Pixel 8, Screenshots) nachgestellt:
Die schwebenden Eck-Knöpfe (Musik unten rechts, Vorschau-Umschalter unten links
für Dogi/VanVan — siehe .music-widget/.vorschau-widget weiter unten) sind
absichtlich IMMER sichtbar (position:fixed, direkte <body>-Kinder, eigener
Stacking-Kontext, unabhängig vom Menü). Bei aufgeklapptem Mobil-Menü liegen sie
dadurch ÜBER den untersten Menüpunkten (Sprachschalter + "Bewerben"-Knopf) und
verdecken/blockieren sie sichtbar. :has() statt einer weiteren main.js-Änderung,
weil beide Widgets keinen gemeinsamen Elternknoten mit .nav-links haben (beide
hängen direkt unter <body>, .nav-links steckt im Header) — reiner CSS-Fix bleibt
an der Stelle, an der auch die restliche Menü-Overlay-Logik dieses Blocks steht. */
body:has(.nav-links.open) .music-widget,
body:has(.nav-links.open) .vorschau-widget {
display: none;
}
} }
/* ---------- Buttons ---------- /* ---------- Buttons ----------
@@ -1444,6 +1603,185 @@ header.site-header {
} }
@media (max-width: 760px) { .hero-grid { grid-template-columns: 1fr; } } @media (max-width: 760px) { .hero-grid { grid-template-columns: 1fr; } }
/* "Event des Jahres" (20.08.2026) — volle Breite wie .hero-grid darüber (eigener Block,
kein Grid-Kind), zeigt bewusst IMMER genau zwei Events nebeneinander. Gleiche
Karten-/Tag-Optik wie die 3 Welt-Kacheln, damit es wie ein natürlicher Abschluss des
Grids wirkt statt wie ein fremdes Element. */
.jahres-events {
margin-top: 1.2rem;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.6rem;
box-shadow: var(--shadow);
}
.jahres-events-head { margin-bottom: 1.2rem; }
.jahres-events-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.2rem;
}
@media (max-width: 760px) { .jahres-events-grid { grid-template-columns: 1fr; } }
/* Nutzer-Wunsch 20.08.2026: "die kacheln sollen auch bissl spezieller sein" —
goldener Trophäen-Akzent statt der neutralen Standard-Kartenoptik (eigener,
gedeckter Farbton, kein grelles Gold — augenschonend), plus ein dezenter
wandernder Lichtschimmer oben (".jahres-event-shine"), der die Kachel als
etwas Besonderes markiert statt als normale Karte. Jetzt ein <button>
(öffnet ein Detail-Fenster) statt eines <a> (navigierte sofort weg) —
deshalb hier zusätzlich ein vollständiger Button-Reset. */
/* [hidden] explizit VOR der Klassenregel + als eigene Regel danach: ein
simples "display:block" in .jahres-event-card hat dieselbe Spezifität
wie die eingebaute [hidden]-UA-Regel des Browsers und würde sie sonst im
Zweifel überschreiben (identischer Bug wie schon bei .jahres-event-bild
weiter unten, hier per Playwright-Screenshot erneut gefunden — eine
"leere" Kachel blieb trotz hidden-Attribut sichtbar). */
.jahres-event-card[hidden] { display: none; }
.jahres-event-card {
display: block;
width: 100%;
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
position: relative;
overflow: hidden;
isolation: isolate;
padding: 1.2rem 1.4rem;
border-radius: calc(var(--radius) - 4px);
border: 1px solid rgba(212, 175, 55, .35);
background:
radial-gradient(circle at 15% 0%, rgba(212, 175, 55, .08), transparent 55%),
rgba(255, 255, 255, .02);
transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
}
.jahres-event-card:hover, .jahres-event-card:focus-visible {
border-color: rgba(212, 175, 55, .7);
transform: translateY(-3px);
box-shadow: 0 10px 26px rgba(0, 0, 0, .35), 0 0 22px rgba(212, 175, 55, .18);
}
.jahres-event-shine {
content: "";
position: absolute;
top: 0; left: 6%; right: 6%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 214, 102, .9), transparent);
animation: jahres-event-goldglanz 4.5s ease-in-out infinite;
pointer-events: none;
}
@keyframes jahres-event-goldglanz { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .jahres-event-shine { animation: none; opacity: .6; } }
.jahres-event-datum {
display: block;
color: #e8c465;
font-weight: 700;
font-size: .82rem;
letter-spacing: .02em;
margin-bottom: .3rem;
}
.jahres-event-card h3 { margin: 0 0 .4rem; font-size: 1.15rem; }
.jahres-event-card p { margin: 0 0 .6rem; color: var(--text-muted); font-size: .93rem; line-height: 1.55; }
.jahres-event-mehr { color: #e8c465; font-weight: 600; font-size: .88rem; }
/* Genau EIN aktives Event: zentrierte, etwas größere Einzelkachel statt
halbleerem Zwei-Spalten-Raster (Nutzer-Wunsch: "spezieller"). */
.jahres-events-grid-einzeln {
grid-template-columns: 1fr;
max-width: 480px;
margin: 0 auto;
}
.jahres-events-grid-einzeln .jahres-event-card { padding: 1.6rem 1.8rem; }
.jahres-events-grid-einzeln .jahres-event-card h3 { font-size: 1.3rem; }
/* Optionales Admin-Bild pro Event (20.08.2026, "Event des Jahres" jetzt in
der Verwaltung pflegbar) — bleibt per [hidden] unsichtbar, solange kein
Bild gesetzt ist, damit die textbasierten Standard-Karten unverändert
aussehen. Bugfix (per Playwright-Test gefunden): "display:block" allein
hätte dieselbe Spezifität wie die eingebaute [hidden]-Regel des Browsers
und würde sie im Zweifel überschreiben (leerer Bild-Rahmen wäre auch OHNE
gesetztes Bild sichtbar gewesen) — deshalb hier zusätzlich explizit
:not([hidden]) statt nur der Klasse allein. */
.jahres-event-bild[hidden] { display: none; }
.jahres-event-bild:not([hidden]) {
display: block;
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
border-radius: calc(var(--radius) - 4px);
margin-bottom: .9rem;
}
/* ---------- "Event des Jahres" — Detail-Fenster (20.08.2026) ----------
Nutzer-Wunsch: "wen ich drauf drücke dass dein ein kleines fenster
aufgeht wo das bild bissl größer ist mit einem größeren text und
beschreibung, und mit einem link ... bereite das richtig geil krank
vor." Augenschonend gehalten (siehe Projektregel): kein grelles Weiß,
sanftes Ein-/Ausblenden statt hartem Umschalten, respektiert
prefers-reduced-motion. */
.jahres-event-modal-overlay {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: 4vh 5vw;
background: rgba(5, 7, 11, .78);
backdrop-filter: blur(6px);
opacity: 0;
transition: opacity .22s ease;
}
.jahres-event-modal-overlay.offen { opacity: 1; }
.jahres-event-modal-overlay[hidden] { display: none; }
body.modal-offen { overflow: hidden; }
.jahres-event-modal {
position: relative;
width: min(640px, 100%);
max-height: 90vh;
overflow-y: auto;
background: var(--card-bg);
border: 1px solid rgba(212, 175, 55, .4);
border-radius: var(--radius);
box-shadow: 0 24px 60px rgba(0, 0, 0, .5), 0 0 40px rgba(212, 175, 55, .12);
transform: scale(.94) translateY(10px);
transition: transform .25s ease;
}
.jahres-event-modal-overlay.offen .jahres-event-modal { transform: scale(1) translateY(0); }
@media (prefers-reduced-motion: reduce) {
.jahres-event-modal-overlay, .jahres-event-modal { transition: none; }
}
.jahres-event-modal-close {
position: absolute;
top: .9rem; right: .9rem;
z-index: 2;
width: 38px; height: 38px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, .25);
background: rgba(5, 7, 11, .7);
color: var(--text);
font-size: 1.1rem;
cursor: pointer;
transition: border-color .15s ease, transform .15s ease;
}
.jahres-event-modal-close:hover { border-color: rgba(212, 175, 55, .7); transform: scale(1.06); }
.jahres-event-modal-bild {
display: block;
width: 100%;
max-height: 46vh;
object-fit: cover;
border-radius: var(--radius) var(--radius) 0 0;
}
.jahres-event-modal-bild[hidden] { display: none; }
.jahres-event-modal-body { padding: 1.8rem 2rem 2.2rem; }
.jahres-event-modal-datum {
display: block;
color: #e8c465;
font-weight: 700;
font-size: .92rem;
letter-spacing: .03em;
margin-bottom: .5rem;
}
.jahres-event-modal-body h2 { font-size: 1.7rem; margin: 0 0 .8rem; }
.jahres-event-modal-body p { font-size: 1.08rem; line-height: 1.7; color: var(--text-muted); margin: 0 0 1.4rem; }
.jahres-event-modal-link[hidden] { display: none; }
/* ---------- Cards ---------- */ /* ---------- Cards ---------- */
.card { .card {
background: var(--card-bg); background: var(--card-bg);
@@ -1645,6 +1983,50 @@ header.site-header {
box-shadow: 0 0 60px 6px rgba(143,217,234,.18); box-shadow: 0 0 60px 6px rgba(143,217,234,.18);
opacity: .85; opacity: .85;
} }
/* "Diese Seite wächst noch"-Hinweis (20.08.2026, Nutzer-Wunsch) — bewusst
einladend statt wie eine Fehler-/Baustellen-Meldung, im selben Babyblau
wie der Rest des Wachstums-Baum-Themas dieser Seite (.zeit-baum-kappe/
-wurzel). Sitzt VOR dem Baum, damit Besucher sofort verstehen: hier
entsteht noch etwas, ohne dass es wie ein Fehler wirkt. */
.zeit-baustelle {
display: flex;
align-items: flex-start;
gap: 1rem;
max-width: 640px;
margin: 0 auto 2.6rem;
padding: 1.1rem 1.4rem;
border-radius: var(--radius);
background: rgba(143, 217, 234, .06);
border: 1.5px solid rgba(143, 217, 234, .35);
box-shadow: 0 0 24px rgba(143, 217, 234, .12);
}
.zeit-baustelle-icon {
flex-shrink: 0;
font-size: 1.8rem;
line-height: 1;
filter: drop-shadow(0 0 8px rgba(143, 217, 234, .55));
animation: zeit-baustelle-wiegen 3.5s ease-in-out infinite;
}
.zeit-baustelle strong {
display: block;
font-size: 1.05rem;
margin-bottom: .3rem;
color: var(--text);
}
.zeit-baustelle p {
margin: 0;
font-size: .92rem;
color: var(--text-muted);
line-height: 1.55;
}
@keyframes zeit-baustelle-wiegen {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(5deg); }
}
@media (prefers-reduced-motion: reduce) {
.zeit-baustelle-icon { animation: none; }
}
.zeit-baum-kappe { .zeit-baum-kappe {
position: relative; position: relative;
display: flex; display: flex;
@@ -1754,6 +2136,16 @@ header.site-header {
.zeit-ast.zeit-final .zeit-karte { width: calc(100% - 4rem); max-width: 640px; text-align: center; } .zeit-ast.zeit-final .zeit-karte { width: calc(100% - 4rem); max-width: 640px; text-align: center; }
.zeit-ast.zeit-final { justify-content: center !important; } .zeit-ast.zeit-final { justify-content: center !important; }
.zeit-ast.zeit-final .zeit-karte::before { display: none; } .zeit-ast.zeit-final .zeit-karte::before { display: none; }
/* Bugfix 20.08.2026 (Nutzer-Report: "wieso dieser Punkt da in der Mitte"):
.zeit-node ist bei allen normalen Karten position:absolute; left:50%;
top:50% relativ zu .zeit-ast — das sitzt korrekt in der Baum-Mitte,
WEIL die Karte dort nur die halbe Breite einnimmt. Die Finale-Karte
(.zeit-final) wird aber auf fast volle Breite gestreckt und zentriert
(siehe .zeit-karte-Regel oben) — der Knoten landete dadurch mitten im
Text statt am Rand. Die Verbindungslinie (::before, direkt drüber)
wurde dafür schon 2025 ausgeblendet, der Knoten selbst wurde dabei
übersehen. */
.zeit-ast.zeit-final .zeit-node { display: none; }
.zeit-zitat { .zeit-zitat {
font-weight: 700; font-weight: 700;
color: var(--text); color: var(--text);
@@ -1968,6 +2360,51 @@ footer.site-footer {
(z.B. dem DogFather-Manager-Artwork) nicht flach wirken. */ (z.B. dem DogFather-Manager-Artwork) nicht flach wirken. */
filter: saturate(1.26) contrast(1.15) brightness(1.03); filter: saturate(1.26) contrast(1.15) brightness(1.03);
} }
/* Sehr hohe Hochkant-Bilder (z.B. streamer-mascot.jpg mit 900x2812 px) würden
mit aspect-ratio:auto + height:auto in voller natürlicher Höhe rendern und die
danebenstehende Textspalte komplett erschlagen (Nutzer-Report 20.08.2026:
"der soll bissl kleiner sein und nicht so riesig"). Ein zu knapper max-height-
Deckel (erster Versuch: 460px) machte es dann aber zu klein und ließ eine
sichtbare Lücke unter dem Bild, weil die Textspalte daneben normalerweise
deutlich mehr Platz braucht (Nutzer-Report 21.08.2026: "die größe vom bild
vom husky soll dem text rechts nebendran angepasst werden").
Ausprobiert und wieder verworfen: die Höhe per CSS Grid align-items:stretch +
height:100% exakt an die Textspalte zu koppeln. Funktioniert bei Grid NICHT
robust, weil ein 'auto' sizing Track seine Höhe am MAX-CONTENT-Beitrag beider
Felder ausrichtet -- bei einem derart extremen Hochkantbild (Seitenverhältnis
~1:3.1) zieht das Bild die ganze Zeile (und damit auch die Textspalte) auf
seine eigene, riesige natürliche Höhe hoch, egal was man an min-height
probiert (min-height wirkt nur auf den Mindest-, nicht den Wachstums-Beitrag).
Robuster Kompromiss stattdessen: ein großzügiger fester max-height-Wert
(720px statt 460px), der sich an der TYPISCHEN Höhe der Textspalte in diesem
Abschnitt orientiert (auf normalen Desktop-Breiten ~715-850px hoch) -- das
Bild wirkt dadurch angemessen groß und auf Augenhöhe mit dem Text, ohne die
fragile/rückkoppelnde Grid-Stretch-Mechanik. width:fit-content lässt den
Rahmen dabei eng am Bild sitzen, statt breite gestreifte Hintergrund-Balken
danebenzulassen.
WICHTIG: muss NACH ".collage-photo img" stehen — beide Regeln haben dieselbe
Spezifität (eine Klasse + ein Element), die spätere gewinnt. Stünde dieser
Block davor, würde width:100%/height:100% oben die Deckelung wieder aufheben. */
.collage-photo-kompakt {
aspect-ratio: auto;
width: fit-content;
max-width: 100%;
margin: 0 auto;
}
.collage-photo-kompakt img {
width: auto;
height: auto;
max-height: 720px;
max-width: 100%;
display: block;
object-fit: contain;
}
@media (max-width: 780px) {
/* Auf Mobilgeräten stehen die Grid-Spalten untereinander -- dort ist die
Textspalte durch die volle Breite viel kürzer, ein 720px hohes Bild wäre
unangemessen groß für einen schmalen Handy-Screen. Deckel daher kleiner. */
.collage-photo-kompakt img { max-height: 420px; }
}
/* Cineastischer "Poster-Grade" für JEDES Präsentationsfoto auf der Seite: /* Cineastischer "Poster-Grade" für JEDES Präsentationsfoto auf der Seite:
zwei Farbschein-Blooms in der jeweiligen Rahmenfarbe (oben-links / zwei Farbschein-Blooms in der jeweiligen Rahmenfarbe (oben-links /
unten-rechts, wie ein Studio-Rimlight — nutzt dieselbe --frame-Variable unten-rechts, wie ein Studio-Rimlight — nutzt dieselbe --frame-Variable
@@ -2146,7 +2583,7 @@ footer.site-footer {
Cover, die jeden Rand auffüllt, den "contain" freilässt — dadurch Cover, die jeden Rand auffüllt, den "contain" freilässt — dadurch
wirkt es trotzdem randlos, obwohl die scharfen Pixel garantiert wirkt es trotzdem randlos, obwohl die scharfen Pixel garantiert
nie beschnitten werden. */ nie beschnitten werden. */
background-image: url("../img/universe-trio.jpg"), url("../img/universe-trio-blur.jpg"); background-image: url("../img/universe-trio.jpg?v=20260820h"), url("../img/universe-trio-blur.jpg?v=20260820h");
background-size: contain, cover; background-size: contain, cover;
background-position: center, center; background-position: center, center;
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
@@ -3188,3 +3625,342 @@ body.start-seite .music-toggle:focus-visible {
margin: 1.5rem 0; margin: 1.5rem 0;
} }
.todo-note strong { color: var(--accent-2); } .todo-note strong { color: var(--accent-2); }
/* ===================================================================
Streamplan — "Live-Radar" (20.08.2026, Nutzer-Wunsch: "soll viel
geiler und spezieller sein, überrasch mich"). Ein 24-Stunden-Ziffernblatt
als SVG: der feste 20-Uhr-Termin leuchtet dauerhaft als Fixpunkt, ein
"Jetzt"-Zeiger bewegt sich in Echtzeit mit der tatsächlichen Uhrzeit,
ein sanft rotierender Lichtschein sorgt für Atmosphäre. Schaltet live
auf Rot/Puls um, sobald DogFather laut /live-status wirklich sendet —
sonst zählt ein echter Sekunden-Countdown zum nächsten 20-Uhr-Termin
runter. Bewusst augenschonend: gedeckte Farben, langsame, sanfte
Animation, kompletter Stillstand bei prefers-reduced-motion. ================================================================= */
/* Reihe für 1-2 Uhren nebeneinander (20.08.2026: zweite Uhr für ein
"spezielles Live-Event", siehe assets/js/streamplan.js) — bricht auf
schmalen Bildschirmen automatisch untereinander um. */
.stream-radar-reihe {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
gap: 2.4rem;
margin-top: 1.4rem;
}
/* .stream-radar-wrap setzt selbst "display", das schlägt bei gleicher
Spezifität die eingebaute [hidden]-Regel des Browsers (identisches,
inzwischen mehrfach gefundenes Muster auf dieser Seite) — deshalb hier
explizit VOR der Klassenregel. */
.stream-radar-wrap[hidden] { display: none; }
.stream-radar-wrap { display: flex; flex-direction: column; align-items: center; margin: 0 auto; }
.stream-radar {
position: relative;
width: min(300px, 70vw);
aspect-ratio: 1;
margin: 0 auto;
}
.stream-radar-sweep {
position: absolute;
inset: -6%;
border-radius: 50%;
background: conic-gradient(from 0deg,
transparent 0deg, rgba(143, 217, 234, .28) 40deg, transparent 90deg,
transparent 200deg, rgba(232, 196, 101, .22) 250deg, transparent 300deg,
transparent 320deg, rgba(139, 92, 246, .24) 350deg, transparent 360deg);
animation: stream-radar-rotate 16s linear infinite;
pointer-events: none;
}
/* Farbvariante fürs spezielle Live-Event (Nutzer-Wunsch "bissl Farbe") —
Magenta/Violett/Gold statt Cyan/Violett/Gold, damit die zweite Uhr auch
auf den ersten Blick als "etwas Besonderes" erkennbar ist. */
.stream-radar.event-spezial .stream-radar-sweep {
background: conic-gradient(from 0deg,
transparent 0deg, rgba(255, 92, 180, .26) 40deg, transparent 90deg,
transparent 200deg, rgba(232, 196, 101, .22) 250deg, transparent 300deg,
transparent 320deg, rgba(139, 92, 246, .28) 350deg, transparent 360deg);
}
@keyframes stream-radar-rotate { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .stream-radar-sweep { animation: none; } }
.stream-radar-dial { position: relative; width: 100%; height: 100%; display: block; }
/* Bunter Verlauf statt reinem Grau-Ton auf dem äußeren Ring (Nutzer-Wunsch
"bissl Farbe") — Farbverlauf steckt als <linearGradient> direkt im SVG
jeder Uhr (siehe streamplan.html), hier nur die Basiswerte/Deckkraft. */
.radar-ring-outer { fill: none; stroke-width: 2; opacity: .8; }
.radar-ring-inner { fill: rgba(255, 255, 255, .02); stroke: rgba(255, 255, 255, .07); stroke-width: 1; }
.radar-tick { stroke: rgba(255, 255, 255, .22); stroke-width: 1.5; }
.radar-tick-major { stroke: rgba(255, 255, 255, .5); stroke-width: 2.2; }
.radar-tick-label { fill: var(--text-muted); font-size: 11px; font-family: var(--font-body, inherit); }
.radar-fixmarker-dot {
fill: #e8c465;
filter: drop-shadow(0 0 6px rgba(232, 196, 101, .9)) drop-shadow(0 0 14px rgba(232, 196, 101, .5));
animation: stream-radar-fixpuls 3.2s ease-in-out infinite;
}
@keyframes stream-radar-fixpuls { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .radar-fixmarker-dot { animation: none; opacity: .9; } }
.radar-now-hand {
stroke: var(--accent);
stroke-width: 2.5;
stroke-linecap: round;
filter: drop-shadow(0 0 4px var(--accent));
transition: x2 .6s ease, y2 .6s ease;
}
.radar-now-dot {
fill: var(--accent);
filter: drop-shadow(0 0 6px var(--accent));
transition: cx .6s ease, cy .6s ease;
}
.stream-radar.ist-live .radar-now-hand,
.stream-radar.ist-live .radar-now-dot { stroke: #ff5c72; fill: #ff5c72; filter: drop-shadow(0 0 8px rgba(255, 92, 114, .8)); }
.radar-center-dot { fill: var(--text); opacity: .8; }
.stream-radar-center {
position: absolute;
inset: 24%;
border-radius: 50%;
background: rgba(8, 10, 14, .82);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: .5rem;
gap: .25rem;
}
.stream-radar-dot {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--text-muted);
margin-bottom: .2rem;
}
.stream-radar.ist-live .stream-radar-dot { background: #ff5c72; box-shadow: 0 0 10px rgba(255, 92, 114, .8); animation: stream-radar-live-puls 1.4s ease-in-out infinite; }
@keyframes stream-radar-live-puls { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.35); opacity: .75; } }
@media (prefers-reduced-motion: reduce) { .stream-radar.ist-live .stream-radar-dot { animation: none; } }
.stream-radar-status { font-size: 1rem; font-weight: 800; color: var(--text); line-height: 1.25; }
.stream-radar-countdown {
font-variant-numeric: tabular-nums;
font-size: 1.55rem;
font-weight: 900;
letter-spacing: .02em;
color: var(--accent);
}
.stream-radar.ist-live .stream-radar-countdown { color: #ff5c72; }
.stream-radar-sub { font-size: .74rem; color: var(--text-muted); }
/* Farbvariante fürs spezielle Live-Event: der Fixpunkt ist hier nicht der
tägliche 20-Uhr-Termin, sondern der eigene Zieltermin -- Magenta statt
Gold, damit man beide Uhren auf einen Blick unterscheidet. */
.stream-radar.event-spezial .radar-fixmarker-dot {
fill: #ff5cb8;
filter: drop-shadow(0 0 6px rgba(255, 92, 184, .9)) drop-shadow(0 0 14px rgba(255, 92, 184, .5));
}
.stream-radar.event-spezial:not(.ist-live) .radar-now-hand,
.stream-radar.event-spezial:not(.ist-live) .radar-now-dot { stroke: #c9a6ff; fill: #c9a6ff; filter: drop-shadow(0 0 5px rgba(201, 166, 255, .8)); }
.stream-radar.event-spezial:not(.ist-live) .stream-radar-countdown { color: #ff5cb8; }
.stream-radar-titel { font-size: .95rem; font-weight: 800; color: #ff9ed4; margin-bottom: .3rem; }
/* .btn setzt selbst "display", das schlägt bei gleicher Spezifität die
eingebaute [hidden]-Regel des Browsers (identisches Muster wie bei
.jahres-event-card weiter oben) — deshalb hier explizit. */
.stream-radar-watch-btn[hidden] { display: none; }
.stream-radar-watch-btn { margin-top: 1.1rem; }
.stream-radar-hinweis { margin-top: 1rem; opacity: .8; }
/* ===================================================================
"Stimmen" — eigene Nachricht einreichen (20.08.2026, Nutzer-Wunsch:
"eine richtig geile kachel ... das wird mega persoenlich zu den fans,
bitte wirklich krass geil speziell"). Warmer Babyblau/Rosé/Lila-
Farbverlauf statt der neutralen Standardkarte, wandernder Lichtschein
am Rand (dieselbe Zwei-Ebenen-Technik wie schon an mehreren Stellen
dieses Projekts, siehe .supp-card::before) — bewusst augenschonend
(gedeckte Farben, sanfte Bewegung, respektiert reduzierte Bewegung). ================================================================= */
/* Nutzer-Wunsch 20.08.2026 (2. Runde): "die kachel soll noch spezieller und
geiler aussehen". Deutlich aufgewertet gegenüber der ersten Fassung:
wandernder Farbverlauf-Rahmen rundherum (Zwei-Ebenen-Technik mit
background-position statt eines rotierenden Rings — siehe dokumentierte
Projekt-Lektion: rotierende Masken verzerren auf eckigen Flächen),
schwebende Herz-Partikel im Hintergrund, Medaillon statt nacktem Emoji,
Farbverlauf-Überschrift und Eingabefelder, die beim Tippen aufleuchten.
Bleibt trotzdem augenschonend: gedeckte Töne, langsame Bewegungen,
vollständiger Stillstand bei prefers-reduced-motion. */
.stimmen-einreichen-kachel {
position: relative;
isolation: isolate;
overflow: hidden;
max-width: 580px;
margin: 0 auto;
padding: 2.4rem 2.4rem 2.6rem;
border-radius: calc(var(--radius) + 4px);
background:
radial-gradient(ellipse at 12% 0%, rgba(255, 154, 208, .13), transparent 58%),
radial-gradient(ellipse at 88% 100%, rgba(143, 217, 234, .13), transparent 58%),
linear-gradient(180deg, rgba(24, 34, 51, .96), rgba(14, 20, 32, .98));
box-shadow: 0 30px 70px rgba(0, 0, 0, .45), 0 0 50px rgba(143, 217, 234, .06);
}
/* Farbverlauf-Rahmen: zwei Ebenen, nur die Hintergrundposition wandert. */
.stimmen-einreichen-kachel::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
padding: 1.5px;
background: linear-gradient(115deg, rgba(255,154,208,.75), rgba(143,217,234,.75), rgba(199,204,209,.5), rgba(255,154,208,.75));
background-size: 300% 100%;
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
animation: stimmen-rahmen-wandert 9s linear infinite;
}
@keyframes stimmen-rahmen-wandert { to { background-position: 300% 0; } }
/* Schwebende Herz-Partikel — reines CSS, keine zusätzlichen Elemente nötig. */
.stimmen-einreichen-glanz {
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(circle, rgba(255,154,208,.5) 2px, transparent 3px),
radial-gradient(circle, rgba(143,217,234,.45) 2px, transparent 3px),
radial-gradient(circle, rgba(255,154,208,.35) 1.5px, transparent 3px);
background-size: 190px 190px, 260px 260px, 140px 140px;
background-position: 12% 18%, 78% 62%, 44% 88%;
animation: stimmen-partikel-treiben 16s ease-in-out infinite alternate;
}
@keyframes stimmen-partikel-treiben {
from { background-position: 12% 18%, 78% 62%, 44% 88%; opacity: .55; }
to { background-position: 18% 26%, 71% 54%, 50% 79%; opacity: .95; }
}
@media (prefers-reduced-motion: reduce) {
.stimmen-einreichen-kachel::before, .stimmen-einreichen-glanz { animation: none; }
}
.stimmen-einreichen-kopf { text-align: center; margin-bottom: 1.8rem; }
/* Medaillon statt nacktem Emoji: Kreis mit Farbschein und sanftem Puls. */
.stimmen-einreichen-herz {
display: inline-flex;
align-items: center;
justify-content: center;
width: 74px; height: 74px;
border-radius: 50%;
font-size: 2.1rem;
background: radial-gradient(circle at 35% 30%, rgba(255,154,208,.28), rgba(143,217,234,.12) 70%);
border: 1.5px solid rgba(255, 154, 208, .45);
box-shadow: 0 0 26px rgba(255, 154, 208, .3), inset 0 0 22px rgba(255, 154, 208, .12);
animation: stimmen-herz-puls 3.4s ease-in-out infinite;
}
@keyframes stimmen-herz-puls {
0%, 100% { transform: scale(1); box-shadow: 0 0 22px rgba(255,154,208,.22), inset 0 0 20px rgba(255,154,208,.1); }
50% { transform: scale(1.06); box-shadow: 0 0 40px rgba(255,154,208,.45), inset 0 0 26px rgba(255,154,208,.18); }
}
@media (prefers-reduced-motion: reduce) { .stimmen-einreichen-herz { animation: none; } }
.stimmen-einreichen-kopf h2 {
margin: .9rem 0 .6rem;
background: linear-gradient(100deg, #ffd0e8, #ffffff 40%, #a8e4f2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.stimmen-einreichen-kopf p { color: var(--text-muted); }
#stimmen-formular .field { margin-bottom: 1.1rem; }
/* Eingabefelder leuchten beim Tippen in der Kachel-Farbwelt auf. */
#stimmen-formular .field input,
#stimmen-formular .field textarea {
transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
#stimmen-formular .field input:focus,
#stimmen-formular .field textarea:focus {
outline: none;
border-color: rgba(255, 154, 208, .75);
box-shadow: 0 0 0 3px rgba(255, 154, 208, .14), 0 0 22px rgba(255, 154, 208, .18);
background: rgba(255, 255, 255, .06);
}
.stimmen-erfolg { color: #baf7d6; font-size: .9rem; margin-top: 1rem; display: none; text-align: center; }
.stimmen-erfolg.zeige { display: block; }
.stimmen-fehler { color: #ff9a9a; font-size: .88rem; margin-top: 1rem; display: none; text-align: center; }
.stimmen-fehler.zeige { display: block; }
/* Freigegebene Stimmen (20.08.2026, 2. Runde: "und auch die die danach
kommen wenn ich sie eingebe die sollen viel geiler sein"): echte
Präsentationskarten statt schlichter Kästen — Farbschein-Bloom in der
Ecke, großes Anführungszeichen als Wasserzeichen, farbige Akzentlinie
oben, Avatar-Medaillon mit dem Anfangsbuchstaben, TikTok-Handle als
eigener Chip. Beim Überfahren hebt sich die Karte leicht an. */
.stimmen-karte {
position: relative;
isolation: isolate;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 2rem 1.8rem 1.6rem;
border-radius: calc(var(--radius) - 2px);
border: 1px solid rgba(143, 217, 234, .22);
background:
radial-gradient(ellipse at 100% 0%, rgba(255, 154, 208, .1), transparent 55%),
linear-gradient(180deg, rgba(24, 34, 51, .9), rgba(15, 21, 33, .95));
box-shadow: 0 16px 40px rgba(0, 0, 0, .32);
transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease;
}
.stimmen-karte:hover {
transform: translateY(-4px);
border-color: rgba(255, 154, 208, .5);
box-shadow: 0 22px 52px rgba(0, 0, 0, .42), 0 0 30px rgba(255, 154, 208, .14);
}
/* Farbige Akzentlinie oben — greift die Farbwelt der Einreich-Kachel auf. */
.stimmen-karte::after {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2.5px;
background: linear-gradient(90deg, rgba(255,154,208,.9), rgba(143,217,234,.9));
}
/* Großes Anführungszeichen als dezentes Wasserzeichen im Hintergrund. */
.stimmen-karte::before {
content: "„";
position: absolute;
top: -.9rem; right: .6rem;
z-index: -1;
font-size: 8rem;
font-family: Georgia, serif;
color: rgba(143, 217, 234, .09);
line-height: 1;
pointer-events: none;
}
.stimmen-karte-text {
position: relative;
z-index: 1;
flex: 1;
margin: 0 0 1.3rem;
line-height: 1.75;
font-size: 1.02rem;
color: var(--text);
}
.stimmen-karte-fuss {
display: flex;
align-items: center;
gap: .7rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, .07);
}
/* Avatar-Medaillon mit dem Anfangsbuchstaben (wird in stimmen.js gefüllt). */
.stimmen-karte-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px; height: 40px;
flex-shrink: 0;
border-radius: 50%;
font-weight: 800;
font-size: 1rem;
color: #0d1420;
background: linear-gradient(135deg, #ffd0e8, #a8e4f2);
box-shadow: 0 0 16px rgba(255, 154, 208, .3);
}
.stimmen-karte-namen { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.stimmen-karte-name { font-weight: 700; color: var(--text); font-size: .95rem; }
.stimmen-karte-tiktok {
align-self: flex-start;
font-size: .76rem;
font-weight: 600;
color: #a8e4f2;
padding: .1rem .5rem;
border-radius: 999px;
background: rgba(143, 217, 234, .12);
border: 1px solid rgba(143, 217, 234, .28);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 239 KiB

+18
View File
@@ -0,0 +1,18 @@
/* =====================================================================
data-manager.js — Manager-Datenobjekt (21.08.2026)
DogFather (Filipe) selbst hat weiterhin seine eigene, feste Seite
(manager.html) — dieses Array ist für WEITERE Manager gedacht, die
künftig dazukommen könnten (siehe zeitreise.html, "Aufbau eines
eigenen Creator-Systems": "Eine langfristige Struktur für Creator,
Scouts und zukünftige Manager"). Noch niemand final geklärt, deshalb
bewusst leer — genau wie data-creator.js, bis echte Namen feststehen.
Einfach nach dem Muster in data-scouts.js Objekte ins Array einfügen,
sobald es so weit ist — manager.html liest automatisch aus.
Felder: siehe data-scouts.js (identisches Muster).
===================================================================== */
const MANAGERS = [];
if (typeof window !== "undefined") window.MANAGERS = MANAGERS;
+5 -5
View File
@@ -68,7 +68,7 @@ const MODIS = [
bioHtml: { bioHtml: {
de: ` de: `
<p>Hallo, ich bin <strong>Diene</strong> und auf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p> <p>Hallo, ich bin <strong>Diene</strong> und auf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p>
<p>Ich bin 37 Jahre alt, verheiratet und Mutter von zwei Kindern. Wer mich etwas besser kennt, weiß, dass ich manchmal ziemlich verpeilt und vergesslich sein kann. Und wer mich noch nicht kennt, wird es vermutlich ziemlich schnell bemerken. Genau das gehört aber auch einfach zu mir.</p> <p>Ich bin im August 1988 geboren, verheiratet und Mutter von zwei Kindern. Wer mich etwas besser kennt, weiß, dass ich manchmal ziemlich verpeilt und vergesslich sein kann. Und wer mich noch nicht kennt, wird es vermutlich ziemlich schnell bemerken. Genau das gehört aber auch einfach zu mir.</p>
<p>Auf TikTok bin ich ungefähr seit dem Jahr 2020 aktiv. Anfangs habe ich hauptsächlich Videos angesehen und gelegentlich eigene Videos für mich erstellt.</p> <p>Auf TikTok bin ich ungefähr seit dem Jahr 2020 aktiv. Anfangs habe ich hauptsächlich Videos angesehen und gelegentlich eigene Videos für mich erstellt.</p>
<p>Ende April 2026 war ich zum ersten Mal in einem Livestream von Dogi. Seit Anfang Mai bin ich regelmäßig – meistens sogar täglich – dabei.</p> <p>Ende April 2026 war ich zum ersten Mal in einem Livestream von Dogi. Seit Anfang Mai bin ich regelmäßig – meistens sogar täglich – dabei.</p>
<h3>Warum bin ich geblieben?</h3> <h3>Warum bin ich geblieben?</h3>
@@ -86,7 +86,7 @@ const MODIS = [
`, `,
"de-CH": ` "de-CH": `
<p>Hoi, ich bi <strong>Diene</strong> und uf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p> <p>Hoi, ich bi <strong>Diene</strong> und uf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p>
<p>Ich bi 37 Jahr alt, verhürotet und Mueter vo zwöi Chind. Wer mich es bitzeli besser kennt, weiss, dass ich mängisch ziemlich verpeilt und vergesslich sii cha. Und wer mich no nöd kennt, wird's wahrschindlich ziemlich schnäll merke. Aber genau das ghört eifach au zu mir dezue.</p> <p>Ich bi im Auguscht 1988 gebore, verhürotet und Mueter vo zwöi Chind. Wer mich es bitzeli besser kennt, weiss, dass ich mängisch ziemlich verpeilt und vergesslich sii cha. Und wer mich no nöd kennt, wird's wahrschindlich ziemlich schnäll merke. Aber genau das ghört eifach au zu mir dezue.</p>
<p>Uf TikTok bin ich öppe sit 2020 aktiv. Am Afang han ich hauptsächlich Videos aglueget und ab und zue mal es eigets Video für mich gmacht.</p> <p>Uf TikTok bin ich öppe sit 2020 aktiv. Am Afang han ich hauptsächlich Videos aglueget und ab und zue mal es eigets Video für mich gmacht.</p>
<p>Ändi April 2026 bin ich zum erste Mal i-me Livestream vom Dogi gsi. Sit Afang Mai bin ich regelmässig – meistens sogar täglich – debii.</p> <p>Ändi April 2026 bin ich zum erste Mal i-me Livestream vom Dogi gsi. Sit Afang Mai bin ich regelmässig – meistens sogar täglich – debii.</p>
<h3>Wieso bin ich blibe?</h3> <h3>Wieso bin ich blibe?</h3>
@@ -104,7 +104,7 @@ const MODIS = [
`, `,
en: ` en: `
<p>Hi, I'm <strong>Diene</strong>, and you can find me on TikTok as <strong>@_ditschi</strong>.</p> <p>Hi, I'm <strong>Diene</strong>, and you can find me on TikTok as <strong>@_ditschi</strong>.</p>
<p>I'm 37 years old, married, and a mother of two. Anyone who knows me a bit better knows that I can be pretty scatterbrained and forgetful sometimes. And anyone who doesn't know me yet will probably notice pretty quickly. But that's just part of who I am.</p> <p>I was born in August 1988, married, and a mother of two. Anyone who knows me a bit better knows that I can be pretty scatterbrained and forgetful sometimes. And anyone who doesn't know me yet will probably notice pretty quickly. But that's just part of who I am.</p>
<p>I've been active on TikTok since around 2020. At first I mostly watched videos and occasionally made a few of my own just for fun.</p> <p>I've been active on TikTok since around 2020. At first I mostly watched videos and occasionally made a few of my own just for fun.</p>
<p>At the end of April 2026, I joined one of Dogi's livestreams for the very first time. Since the beginning of May, I've been there regularly — usually even daily.</p> <p>At the end of April 2026, I joined one of Dogi's livestreams for the very first time. Since the beginning of May, I've been there regularly — usually even daily.</p>
<h3>Why did I stay?</h3> <h3>Why did I stay?</h3>
@@ -122,7 +122,7 @@ const MODIS = [
`, `,
fr: ` fr: `
<p>Salut, je suis <strong>Diene</strong> et vous me trouverez sur TikTok sous le pseudo <strong>@_ditschi</strong>.</p> <p>Salut, je suis <strong>Diene</strong> et vous me trouverez sur TikTok sous le pseudo <strong>@_ditschi</strong>.</p>
<p>J'ai 37 ans, je suis mariée et maman de deux enfants. Ceux qui me connaissent un peu mieux savent que je peux être plutôt tête en l'air et distraite. Et ceux qui ne me connaissent pas encore le remarqueront sans doute assez vite. Mais ça fait tout simplement partie de moi.</p> <p>Je suis née en août 1988, mariée et maman de deux enfants. Ceux qui me connaissent un peu mieux savent que je peux être plutôt tête en l'air et distraite. Et ceux qui ne me connaissent pas encore le remarqueront sans doute assez vite. Mais ça fait tout simplement partie de moi.</p>
<p>Je suis active sur TikTok depuis environ 2020. Au début, je regardais surtout des vidéos et je créais de temps en temps les miennes, juste pour moi.</p> <p>Je suis active sur TikTok depuis environ 2020. Au début, je regardais surtout des vidéos et je créais de temps en temps les miennes, juste pour moi.</p>
<p>Fin avril 2026, j'ai assisté pour la première fois à un livestream de Dogi. Depuis début mai, j'y suis régulièrement — le plus souvent même tous les jours.</p> <p>Fin avril 2026, j'ai assisté pour la première fois à un livestream de Dogi. Depuis début mai, j'y suis régulièrement — le plus souvent même tous les jours.</p>
<h3>Pourquoi suis-je restée ?</h3> <h3>Pourquoi suis-je restée ?</h3>
@@ -140,7 +140,7 @@ const MODIS = [
`, `,
pt: ` pt: `
<p>Olá, eu sou a <strong>Diene</strong> e podem encontrar-me no TikTok como <strong>@_ditschi</strong>.</p> <p>Olá, eu sou a <strong>Diene</strong> e podem encontrar-me no TikTok como <strong>@_ditschi</strong>.</p>
<p>Tenho 37 anos, sou casada e mãe de dois filhos. Quem me conhece um pouco melhor sabe que às vezes posso ser bastante distraída e esquecida. E quem ainda não me conhece, provavelmente vai reparar nisso bem depressa. Mas isso também faz simplesmente parte de mim.</p> <p>Nasci em agosto de 1988, sou casada e mãe de dois filhos. Quem me conhece um pouco melhor sabe que às vezes posso ser bastante distraída e esquecida. E quem ainda não me conhece, provavelmente vai reparar nisso bem depressa. Mas isso também faz simplesmente parte de mim.</p>
<p>Estou ativa no TikTok desde cerca de 2020. No início, via sobretudo vídeos e, de vez em quando, criava os meus próprios, só para mim.</p> <p>Estou ativa no TikTok desde cerca de 2020. No início, via sobretudo vídeos e, de vez em quando, criava os meus próprios, só para mim.</p>
<p>No final de abril de 2026, assisti pela primeira vez a uma livestream do Dogi. Desde o início de maio que estou presente regularmente — na maioria das vezes até diariamente.</p> <p>No final de abril de 2026, assisti pela primeira vez a uma livestream do Dogi. Desde o início de maio que estou presente regularmente — na maioria das vezes até diariamente.</p>
<h3>Porque é que fiquei?</h3> <h3>Porque é que fiquei?</h3>
+19 -8
View File
@@ -74,7 +74,7 @@ const SCOUTS = [
<p>Als Scout unterstütze ich das Team dabei, neue Talente zu entdecken, ihnen zur Seite zu stehen und sie auf ihrem persönlichen Weg zu begleiten. Dabei geht es mir nicht nur um Zahlen oder Erfolge, sondern vor allem um die Menschen, ihre Ziele und ihre Entwicklung.</p> <p>Als Scout unterstütze ich das Team dabei, neue Talente zu entdecken, ihnen zur Seite zu stehen und sie auf ihrem persönlichen Weg zu begleiten. Dabei geht es mir nicht nur um Zahlen oder Erfolge, sondern vor allem um die Menschen, ihre Ziele und ihre Entwicklung.</p>
<p>Ich freue mich auf alles, was noch vor mir liegt: neue Begegnungen, besondere Erinnerungen und viele weitere gemeinsame Momente.</p> <p>Ich freue mich auf alles, was noch vor mir liegt: neue Begegnungen, besondere Erinnerungen und viele weitere gemeinsame Momente.</p>
<p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nicht perfekt, manchmal mit einem grimmigen Blick, aber immer mit Herz. ❤️</p> <p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nicht perfekt, manchmal mit einem grimmigen Blick, aber immer mit Herz. ❤️</p>
<p style="text-align:right;"><strong>– euer Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– euer Patrick ❤️</strong><br />Scout von Dogfather</p>
`, `,
"de-CH": ` "de-CH": `
<p>Im Oktober 2024 han ich schliesslich de nöchscht Schritt gwagt und bi zum erste Mal live gange. Sithär han ich zahlriichi Mönsche cha kenneleärne und e paar dervo fescht i mis Herz gschlosse. Genau die Begägnige und dr persönlich Uustuusch mached TikTok für mich zu öppisem Bsundrigem.</p> <p>Im Oktober 2024 han ich schliesslich de nöchscht Schritt gwagt und bi zum erste Mal live gange. Sithär han ich zahlriichi Mönsche cha kenneleärne und e paar dervo fescht i mis Herz gschlosse. Genau die Begägnige und dr persönlich Uustuusch mached TikTok für mich zu öppisem Bsundrigem.</p>
@@ -83,7 +83,7 @@ const SCOUTS = [
<p>Als Scout unterstütz ich s Team derbi, neui Talänt z'entdecke, ihne zur Syte z'stah und sie uf ihrem persönliche Wäg z'begleite. Debii gaht's mir nöd nume um Zahle oder Erfolg, sondern vor allem um d Mönsche, ihri Ziel und ihri Entwicklig.</p> <p>Als Scout unterstütz ich s Team derbi, neui Talänt z'entdecke, ihne zur Syte z'stah und sie uf ihrem persönliche Wäg z'begleite. Debii gaht's mir nöd nume um Zahle oder Erfolg, sondern vor allem um d Mönsche, ihri Ziel und ihri Entwicklig.</p>
<p>Ich fröi mich uf alles, wo no vor mir liit: neui Begägnige, bsundrigi Erinnerige und no vill gmeinsami Momänt.</p> <p>Ich fröi mich uf alles, wo no vor mir liit: neui Begägnige, bsundrigi Erinnerige und no vill gmeinsami Momänt.</p>
<p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nöd perfekt, mängisch mit emene grimmige Blick, aber immer mit Herz. ❤️</p> <p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nöd perfekt, mängisch mit emene grimmige Blick, aber immer mit Herz. ❤️</p>
<p style="text-align:right;"><strong>– öie Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– öie Patrick ❤️</strong><br />Scout vom Dogfather</p>
`, `,
en: ` en: `
<p>In October 2024, I finally took the next step and went live for the first time. Since then, I've gotten to know so many people, and some of them have found a firm place in my heart. It's exactly these encounters and the personal exchange that make TikTok something special to me.</p> <p>In October 2024, I finally took the next step and went live for the first time. Since then, I've gotten to know so many people, and some of them have found a firm place in my heart. It's exactly these encounters and the personal exchange that make TikTok something special to me.</p>
@@ -92,7 +92,7 @@ const SCOUTS = [
<p>As a scout, I help the team discover new talent, stand by their side, and guide them on their personal journey. For me, it's not just about numbers or success — it's above all about the people, their goals, and their growth.</p> <p>As a scout, I help the team discover new talent, stand by their side, and guide them on their personal journey. For me, it's not just about numbers or success — it's above all about the people, their goals, and their growth.</p>
<p>I'm looking forward to everything still ahead of me: new encounters, special memories, and many more moments together.</p> <p>I'm looking forward to everything still ahead of me: new encounters, special memories, and many more moments together.</p>
<p class="quote" style="margin-top:1rem;">That's me – Patrick. Not perfect, sometimes with a grumpy look, but always with heart. ❤️</p> <p class="quote" style="margin-top:1rem;">That's me – Patrick. Not perfect, sometimes with a grumpy look, but always with heart. ❤️</p>
<p style="text-align:right;"><strong>– your Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– your Patrick ❤️</strong><br />Scout for Dogfather</p>
`, `,
fr: ` fr: `
<p>En octobre 2024, j'ai finalement franchi l'étape suivante et j'ai fait mon premier live. Depuis, j'ai pu rencontrer de nombreuses personnes et certaines d'entre elles ont pris une vraie place dans mon cœur. Ce sont justement ces rencontres et ces échanges personnels qui rendent TikTok si particulier pour moi.</p> <p>En octobre 2024, j'ai finalement franchi l'étape suivante et j'ai fait mon premier live. Depuis, j'ai pu rencontrer de nombreuses personnes et certaines d'entre elles ont pris une vraie place dans mon cœur. Ce sont justement ces rencontres et ces échanges personnels qui rendent TikTok si particulier pour moi.</p>
@@ -101,7 +101,7 @@ const SCOUTS = [
<p>En tant que scout, j'aide l'équipe à découvrir de nouveaux talents, à les épauler et à les accompagner sur leur propre chemin. Ce qui compte pour moi, ce n'est pas seulement les chiffres ou le succès, mais avant tout les personnes, leurs objectifs et leur évolution.</p> <p>En tant que scout, j'aide l'équipe à découvrir de nouveaux talents, à les épauler et à les accompagner sur leur propre chemin. Ce qui compte pour moi, ce n'est pas seulement les chiffres ou le succès, mais avant tout les personnes, leurs objectifs et leur évolution.</p>
<p>Je me réjouis de tout ce qui m'attend encore : de nouvelles rencontres, des souvenirs particuliers et bien d'autres moments partagés.</p> <p>Je me réjouis de tout ce qui m'attend encore : de nouvelles rencontres, des souvenirs particuliers et bien d'autres moments partagés.</p>
<p class="quote" style="margin-top:1rem;">C'est moi, Patrick. Pas parfait, parfois avec un regard grognon, mais toujours avec du cœur. ❤️</p> <p class="quote" style="margin-top:1rem;">C'est moi, Patrick. Pas parfait, parfois avec un regard grognon, mais toujours avec du cœur. ❤️</p>
<p style="text-align:right;"><strong>– votre Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– votre Patrick ❤️</strong><br />Scout de Dogfather</p>
`, `,
pt: ` pt: `
<p>Em outubro de 2024, finalmente dei o próximo passo e fiz a minha primeira live. Desde então, pude conhecer muitas pessoas e algumas delas conquistaram um lugar especial no meu coração. São exatamente esses encontros e a troca pessoal que tornam o TikTok algo especial para mim.</p> <p>Em outubro de 2024, finalmente dei o próximo passo e fiz a minha primeira live. Desde então, pude conhecer muitas pessoas e algumas delas conquistaram um lugar especial no meu coração. São exatamente esses encontros e a troca pessoal que tornam o TikTok algo especial para mim.</p>
@@ -110,11 +110,21 @@ const SCOUTS = [
<p>Como scout, ajudo a equipe a descobrir novos talentos, a estar ao lado deles e a acompanhá-los no seu caminho pessoal. Para mim, não se trata apenas de números ou sucesso, mas sobretudo das pessoas, dos seus objetivos e do seu desenvolvimento.</p> <p>Como scout, ajudo a equipe a descobrir novos talentos, a estar ao lado deles e a acompanhá-los no seu caminho pessoal. Para mim, não se trata apenas de números ou sucesso, mas sobretudo das pessoas, dos seus objetivos e do seu desenvolvimento.</p>
<p>Estou animado com tudo o que ainda está por vir: novos encontros, memórias especiais e muitos outros momentos em conjunto.</p> <p>Estou animado com tudo o que ainda está por vir: novos encontros, memórias especiais e muitos outros momentos em conjunto.</p>
<p class="quote" style="margin-top:1rem;">Este sou eu – Patrick. Não sou perfeito, às vezes com um olhar carrancudo, mas sempre com coração. ❤️</p> <p class="quote" style="margin-top:1rem;">Este sou eu – Patrick. Não sou perfeito, às vezes com um olhar carrancudo, mas sempre com coração. ❤️</p>
<p style="text-align:right;"><strong>– o seu Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– o seu Patrick ❤️</strong><br />Scout do Dogfather</p>
`, `,
}, },
quote: "", // TODO: Zitat von Patrick einholen quote: {
social: {}, de: "Wege entstehen dadurch, dass man sie geht.",
"de-CH": "Wäg entstönd defür, dass mer sie gaht.",
en: "Paths are created by walking them.",
fr: "Les chemins se créent en marchant.",
pt: "Os caminhos se fazem ao caminhar.",
},
// TikTok-Button auf dem Profil (Nutzer-Wunsch 21.08.2026) -- profil.html
// rendert daraus automatisch den echten .btn-tiktok-Button unter dem
// Namen/Rolle-Text, kein weiterer Code-Umbau nötig (siehe socialLabels
// in profil.html).
social: { tiktok: "https://www.tiktok.com/@patrick180585" },
status: "aktiv", status: "aktiv",
}, },
{ {
@@ -294,7 +304,8 @@ const SCOUTS = [
de: "Ein Mann, ein Wort.", "de-CH": "Bi mir gilt: gseit isch gseit.", de: "Ein Mann, ein Wort.", "de-CH": "Bi mir gilt: gseit isch gseit.",
en: "My word is my bond.", fr: "Chose promise, chose due.", pt: "A minha palavra é a minha honra.", en: "My word is my bond.", fr: "Chose promise, chose due.", pt: "A minha palavra é a minha honra.",
}, },
social: {}, // TikTok-Button auf dem Profil (Nutzer-Wunsch 21.08.2026), siehe Kommentar bei Patrick oben.
social: { tiktok: "https://www.tiktok.com/@bananenstift009" },
status: "aktiv", status: "aktiv",
}, },
// TODO: Dogi liefert die vollständige, aktuelle Scout-Liste (Namen + // TODO: Dogi liefert die vollständige, aktuelle Scout-Liste (Namen +
+8 -5
View File
@@ -29,12 +29,15 @@ window.I18N_PAGE = {
fr: "Cet abonnement n'est ni une boutique ordinaire ni un simple achat. Il est destiné aux personnes qui soutiennent Dogfather durablement et souhaitent faire partie de ce voyage commun.", fr: "Cet abonnement n'est ni une boutique ordinaire ni un simple achat. Il est destiné aux personnes qui soutiennent Dogfather durablement et souhaitent faire partie de ce voyage commun.",
pt: "Esta assinatura não é uma loja comum nem uma compra simples. É para as pessoas que apoiam Dogfather a longo prazo e querem ser parte especial dessa jornada em comum.", pt: "Esta assinatura não é uma loja comum nem uma compra simples. É para as pessoas que apoiam Dogfather a longo prazo e querem ser parte especial dessa jornada em comum.",
}, },
/* Bugfix/Nutzer-Wunsch 20.08.2026: exakter Preis entfernt (nur die erste
Teilphrase geändert, Rest jeder Sprachfassung unangetastet), solange die
Registrierung gesperrt ist — siehe .btn-gold-locked/abo-preis-zahl. */
ab_text2: { ab_text2: {
de: "Für 4,99 € im Monat erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.", de: "Mit deinem monatlichen Beitrag erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.",
"de-CH": "Für 4,99 € im Monat überchunnsch du Zugang zu emene exklusive Supporter-Bereich, bsundere Inhalt und ere persönliche Treuekollektion. Mit jedem erfolgriich bezahlte Meilestei wachst dini Sammlig: vom chline Hase-Teddy über bsundere Jubiläumsprämie bis zum exklusive Hoodie nach 30 Monet. Dernah fangt für dich e neui Kollektion a — mit neue Designs, neue Erinnerige und derselbe Bedeutig.", "de-CH": "Mit dim monetliche Bytrag überchunnsch du Zugang zu emene exklusive Supporter-Bereich, bsundere Inhalt und ere persönliche Treuekollektion. Mit jedem erfolgriich bezahlte Meilestei wachst dini Sammlig: vom chline Hase-Teddy über bsundere Jubiläumsprämie bis zum exklusive Hoodie nach 30 Monet. Dernah fangt für dich e neui Kollektion a — mit neue Designs, neue Erinnerige und derselbe Bedeutig.",
en: "For 4.99 € a month, you get access to an exclusive supporter area, special content, and a personal loyalty collection. With every successfully paid milestone, your collection grows — from the small bunny teddy through special anniversary rewards up to the exclusive hoodie after 30 months. After that, a new collection begins for you, with new designs, new memories, and the same meaning.", en: "With your monthly contribution, you get access to an exclusive supporter area, special content, and a personal loyalty collection. With every successfully paid milestone, your collection grows — from the small bunny teddy through special anniversary rewards up to the exclusive hoodie after 30 months. After that, a new collection begins for you, with new designs, new memories, and the same meaning.",
fr: "Pour 4,99 € par mois, tu as accès à un espace supporter exclusif, à du contenu spécial et à une collection de fidélité personnelle. Avec chaque mois payé avec succès, ta collection grandit : du petit lapin en peluche aux récompenses d'anniversaire jusqu'au hoodie exclusif après 30 mois. Ensuite, une nouvelle collection commence pour toi — avec de nouveaux designs, de nouveaux souvenirs et la même signification.", fr: "Avec ta contribution mensuelle, tu as accès à un espace supporter exclusif, à du contenu spécial et à une collection de fidélité personnelle. Avec chaque mois payé avec succès, ta collection grandit : du petit lapin en peluche aux récompenses d'anniversaire jusqu'au hoodie exclusif après 30 mois. Ensuite, une nouvelle collection commence pour toi — avec de nouveaux designs, de nouveaux souvenirs et la même signification.",
pt: "Por 4,99 € por mês, você tem acesso a uma área exclusiva de apoiadores, conteúdo especial e uma coleção pessoal de fidelidade. A cada mês pago com sucesso, sua coleção cresce: do pequeno coelhinho de pelúcia até recompensas especiais de aniversário e o hoodie exclusivo após 30 meses. Depois disso, começa uma nova coleção para você — com novos designs, novas memórias e o mesmo significado.", pt: "Com sua contribuição mensal, você tem acesso a uma área exclusiva de apoiadores, conteúdo especial e uma coleção pessoal de fidelidade. A cada mês pago com sucesso, sua coleção cresce: do pequeno coelhinho de pelúcia até recompensas especiais de aniversário e o hoodie exclusivo após 30 meses. Depois disso, começa uma nova coleção para você — com novos designs, novas memórias e o mesmo significado.",
}, },
ab_vorteil1: { ab_vorteil1: {
de: "Exklusiver Supporter-Bereich", "de-CH": "Exklusive Supporter-Bereich", de: "Exklusiver Supporter-Bereich", "de-CH": "Exklusive Supporter-Bereich",
+11 -5
View File
@@ -8,12 +8,18 @@ window.I18N_PAGE = {
de: "Werde Teil vom Team", "de-CH": "Wird Teil vom Team", de: "Werde Teil vom Team", "de-CH": "Wird Teil vom Team",
en: "Join the team", fr: "Rejoins l'équipe", pt: "Faça parte da equipe", en: "Join the team", fr: "Rejoins l'équipe", pt: "Faça parte da equipe",
}, },
// Nutzer-Wunsch 21.08.2026: das letzte Wort stand auf manchen Bildschirmbreiten
// ganz allein in einer eigenen Zeile ("Weg" unter sich, siehe Screenshot) --
// sieht typografisch unruhig aus. &nbsp; zwischen den letzten zwei Wörtern
// verhindert, dass genau DAZWISCHEN umgebrochen wird: fällt eine Zeile um,
// dann nehmen die letzten zwei Wörter sich gegenseitig mit statt eines davon
// einsam zurückzulassen.
bw_h1: { bw_h1: {
de: "Community oder Agentur — such dir deinen Weg", de: "Community oder Agentur — such dir deinen&nbsp;Weg",
"de-CH": "Community oder Agentur — such dir dis Wäg", "de-CH": "Community oder Agentur — such dir dis&nbsp;Wäg",
en: "Community or agency — find your path", en: "Community or agency — find your&nbsp;path",
fr: "Communauté ou agence — trouve ta voie", fr: "Communauté ou agence — trouve ta&nbsp;voie",
pt: "Comunidade ou agência — encontre o seu caminho", pt: "Comunidade ou agência — encontre o seu&nbsp;caminho",
}, },
bw_lead: { bw_lead: {
de: "Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.", de: "Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.",
-8
View File
@@ -38,14 +38,6 @@ window.I18N_PAGE = {
de: "Discord beitreten", "de-CH": "Bi Discord mitmache", de: "Discord beitreten", "de-CH": "Bi Discord mitmache",
en: "Join Discord", fr: "Rejoindre le Discord", pt: "Entrar no Discord", en: "Join Discord", fr: "Rejoindre le Discord", pt: "Entrar no Discord",
}, },
com_bild_platzhalter: {
de: "Community-Bild", "de-CH": "Community-Bild",
en: "Community Photo", fr: "Photo de la communauté", pt: "Foto da comunidade",
},
com_foto_folgt: {
de: "Foto folgt", "de-CH": "Foto chunnt no",
en: "Photo coming soon", fr: "Photo à venir", pt: "Foto em breve",
},
com_story_h2: { com_story_h2: {
de: "Team Dogi – mehr als eine Community 🩵", "de-CH": "Team Dogi – meh als e Community 🩵", de: "Team Dogi – mehr als eine Community 🩵", "de-CH": "Team Dogi – meh als e Community 🩵",
en: "Team Dogi – More Than a Community 🩵", fr: "Team Dogi – bien plus qu'une communauté 🩵", pt: "Team Dogi – mais do que uma comunidade 🩵", en: "Team Dogi – More Than a Community 🩵", fr: "Team Dogi – bien plus qu'une communauté 🩵", pt: "Team Dogi – mais do que uma comunidade 🩵",
+7
View File
@@ -39,6 +39,13 @@ window.I18N_PAGE = {
de: "Profil ansehen", "de-CH": "Profil aluege", de: "Profil ansehen", "de-CH": "Profil aluege",
en: "View profile", fr: "Voir le profil", pt: "Ver perfil", en: "View profile", fr: "Voir le profil", pt: "Ver perfil",
}, },
/* 21.08.2026: Fallback-Kategorie für über die Verwaltung angelegte
Creator ohne eigenes Genre (die Verwaltung fragt aktuell keine
Genre-Kategorie ab) -- verhindert einen "undefined"-Filterknopf. */
crt_kategorie_sonstige: {
de: "Sonstige", "de-CH": "Anders",
en: "Other", fr: "Autre", pt: "Outro",
},
crt_bewerben_btn: { crt_bewerben_btn: {
de: "Als Creator bewerben", "de-CH": "Als Creator bewärbe", de: "Als Creator bewerben", "de-CH": "Als Creator bewärbe",
en: "Apply as a creator", fr: "Postuler comme créateur", pt: "Candidatar-se como creator", en: "Apply as a creator", fr: "Postuler comme créateur", pt: "Candidatar-se como creator",
+4
View File
@@ -37,6 +37,10 @@ window.I18N_PAGE = {
de: "🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie", "de-CH": "🐰 HasiDog – E eigeti Wält vollr Chaos, Herz und Fantasie", de: "🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie", "de-CH": "🐰 HasiDog – E eigeti Wält vollr Chaos, Herz und Fantasie",
en: "🐰 HasiDog – A world of its own, full of chaos, heart and imagination", fr: "🐰 HasiDog – Un monde à part, plein de chaos, de cœur et d'imagination", pt: "🐰 HasiDog – Um mundo à parte, cheio de caos, coração e imaginação", en: "🐰 HasiDog – A world of its own, full of chaos, heart and imagination", fr: "🐰 HasiDog – Un monde à part, plein de chaos, de cœur et d'imagination", pt: "🐰 HasiDog – Um mundo à parte, cheio de caos, coração e imaginação",
}, },
hd_btn_tiktok: {
de: "Zur HasiDog TikTok-Seite", "de-CH": "Zur HasiDog TikTok-Site",
en: "Go to HasiDog's TikTok page", fr: "Voir la page TikTok de HasiDog", pt: "Ir para a página TikTok do HasiDog",
},
hd_welt_1: { hd_welt_1: {
de: "HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵", de: "HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵",
"de-CH": "HasiDog isch meh als nur e Figur. Hinter HasiDog steckt e eigeti Wält us Humor, Musig, Gschichte, Überraschige und liebenswerte Chaos. 🩵", "de-CH": "HasiDog isch meh als nur e Figur. Hinter HasiDog steckt e eigeti Wält us Humor, Musig, Gschichte, Überraschige und liebenswerte Chaos. 🩵",
+17 -8
View File
@@ -81,6 +81,23 @@ window.I18N_PAGE = {
fr: "En tant que manager chez Spicy Media : scouts, creators et une collaboration équitable, d'égal à égal.", fr: "En tant que manager chez Spicy Media : scouts, creators et une collaboration équitable, d'égal à égal.",
pt: "Como manager na Spicy Media: scouts, creators e uma parceria justa entre iguais.", pt: "Como manager na Spicy Media: scouts, creators e uma parceria justa entre iguais.",
}, },
/* ---------- "Event des Jahres" (20.08.2026, Nutzer-Wunsch) — volle Breite unter den
3 Welt-Kacheln, zeigt bewusst IMMER genau zwei Events. Texte sind gekürzte, inhaltlich
unveränderte Fassungen der ausführlichen Beschreibungen aus i18n-zeitreise.js
(zr_ev4_h3/p, zr_ev9_h3/p) — dieselben zwei Events, nur kompakter für die Kachel. ---------- */
idx_jahres_events_tag: {
de: "🏆 Event des Jahres", "de-CH": "🏆 Event vom Jahr",
en: "🏆 Event of the Year", fr: "🏆 Événement de l'année", pt: "🏆 Evento do ano",
},
idx_jahres_events_mehr: {
de: "Mehr erfahren →", "de-CH": "Mehr erfahre →",
en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →",
},
idx_jahres_event_modal_link: {
de: "Direkt ansehen →", "de-CH": "Direkt aluege →",
en: "View directly →", fr: "Voir directement →", pt: "Ver diretamente →",
},
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",
en: "🩵 The Faces of the Worlds", fr: "🩵 Les visages des mondes", pt: "🩵 Os rostos dos mundos", en: "🩵 The Faces of the Worlds", fr: "🩵 Les visages des mondes", pt: "🩵 Os rostos dos mundos",
@@ -187,14 +204,6 @@ window.I18N_PAGE = {
de: "Modi-Team ansehen", "de-CH": "Modi-Team aluege", de: "Modi-Team ansehen", "de-CH": "Modi-Team aluege",
en: "View Modi-Team", fr: "Voir l'équipe Modi", pt: "Ver equipe Modi", en: "View Modi-Team", fr: "Voir l'équipe Modi", pt: "Ver equipe Modi",
}, },
idx_aktionen_h2: {
de: "Was gerade läuft", "de-CH": "Was grad lauft",
en: "What's happening right now", fr: "Ce qui se passe en ce moment", pt: "O que está rolando agora",
},
idx_aktionen_btn: {
de: "Alle Aktionen & Projekte", "de-CH": "Alli Aktione & Projekt",
en: "All actions & projects", fr: "Toutes les actions et projets", pt: "Todas as ações e projetos",
},
idx_galerie_h2: { de: "Impressionen", "de-CH": "Impressione", en: "Impressions", fr: "Aperçus", pt: "Impressões" }, idx_galerie_h2: { de: "Impressionen", "de-CH": "Impressione", en: "Impressions", fr: "Aperçus", pt: "Impressões" },
idx_galerie_btn: { idx_galerie_btn: {
de: "Zur ganzen Galerie", "de-CH": "Zur ganze Galerie", de: "Zur ganzen Galerie", "de-CH": "Zur ganze Galerie",
+12
View File
@@ -177,4 +177,16 @@ window.I18N_PAGE = {
fr: "Les scouts trouvent et accompagnent de nouveaux talents pour Dogi.", fr: "Les scouts trouvent et accompagnent de nouveaux talents pour Dogi.",
pt: "Os scouts encontram e acompanham novos talentos para o Dogi.", pt: "Os scouts encontram e acompanham novos talentos para o Dogi.",
}, },
/* 21.08.2026: Abschnitt für weitere, über die Verwaltung angelegte Manager
(siehe zeitreise.html, "Aufbau eines eigenen Creator-Systems" ->
"zukünftige Manager"). Bleibt komplett unsichtbar, solange keiner
eingetragen ist -- siehe renderWeitereManager() weiter unten. */
mgr_weitere_h2: {
de: "Weitere Manager", "de-CH": "Witeri Manager",
en: "More Managers", fr: "Autres managers", pt: "Outros managers",
},
mgr_weitere_profil_btn: {
de: "Profil ansehen", "de-CH": "Profil aluege",
en: "View profile", fr: "Voir le profil", pt: "Ver perfil",
},
}; };
+8
View File
@@ -30,6 +30,10 @@ window.I18N_PAGE = {
de: "Manager DogFather — Creator", "de-CH": "Manager DogFather — Creator", de: "Manager DogFather — Creator", "de-CH": "Manager DogFather — Creator",
en: "Manager DogFather — Creator", fr: "Manager DogFather — Creator", pt: "Manager DogFather — Creator", en: "Manager DogFather — Creator", fr: "Manager DogFather — Creator", pt: "Manager DogFather — Creator",
}, },
pr_eyebrow_manager: {
de: "Manager DogFather — Spicy Media", "de-CH": "Manager DogFather — Spicy Media",
en: "Manager DogFather — Spicy Media", fr: "Manager DogFather — Spicy Media", pt: "Manager DogFather — Spicy Media",
},
pr_back_modis: { pr_back_modis: {
de: "← Zurück zum Modi-Team", "de-CH": "← Zrugg zum Modi-Team", de: "← Zurück zum Modi-Team", "de-CH": "← Zrugg zum Modi-Team",
en: "← Back to the Modi Team", fr: "← Retour à l'équipe Modi", pt: "← Voltar à equipe Modi", en: "← Back to the Modi Team", fr: "← Retour à l'équipe Modi", pt: "← Voltar à equipe Modi",
@@ -42,6 +46,10 @@ window.I18N_PAGE = {
de: "← Zurück zu den Creatorn", "de-CH": "← Zrugg zu de Creator", de: "← Zurück zu den Creatorn", "de-CH": "← Zrugg zu de Creator",
en: "← Back to the Creators", fr: "← Retour aux Creators", pt: "← Voltar aos Creators", en: "← Back to the Creators", fr: "← Retour aux Creators", pt: "← Voltar aos Creators",
}, },
pr_back_manager: {
de: "← Zurück zu Manager DogFather", "de-CH": "← Zrugg zu Manager DogFather",
en: "← Back to Manager DogFather", fr: "← Retour à Manager DogFather", pt: "← Voltar a Manager DogFather",
},
pr_badge_prefix: { pr_badge_prefix: {
de: "Stufe", "de-CH": "Stuefe", de: "Stufe", "de-CH": "Stuefe",
en: "Level", fr: "Niveau", pt: "Nível", en: "Level", fr: "Niveau", pt: "Nível",
+37 -27
View File
@@ -16,37 +16,47 @@ window.I18N_PAGE = {
de: "Was Community und Team sagen.", "de-CH": "Was d Community und s Team säge.", de: "Was Community und Team sagen.", "de-CH": "Was d Community und s Team säge.",
en: "What the community and the team say.", fr: "Ce que disent la communauté et l'équipe.", pt: "O que a comunidade e a equipe dizem.", en: "What the community and the team say.", fr: "Ce que disent la communauté et l'équipe.", pt: "O que a comunidade e a equipe dizem.",
}, },
st_quote_community: { /* Eigene Stimme einreichen (20.08.2026) */
de: `„Hier steht bald eine echte Stimme aus der Community. 🩵"`, st_form_titel: {
"de-CH": `„Da staht bald e echti Stimm us de Community. 🩵"`, de: "Deine Stimme zählt", "de-CH": "Dini Stimm zählt",
en: `"A real voice from the community will be here soon. 🩵"`, en: "Your Voice Matters", fr: "Ta voix compte", pt: "Sua voz importa",
fr: `« Un vrai témoignage de la communauté arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real da comunidade estará aqui. 🩵"`,
}, },
st_label_community: { st_form_text: {
de: "— Community-Mitglied", "de-CH": "— Community-Mitglied", de: "Was bedeutet dir Team Dogi? Schreib uns deine Geschichte — mit etwas Glück liest DogFather sie persönlich und stellt sie hier auf die Seite.",
en: "— Community Member", fr: "— Membre de la communauté", pt: "— Membro da comunidade", "de-CH": "Was bedütet dir Team Dogi? Schriib eus dini Gschicht — mit chli Glück list DogFather sie persönlich und stellt sie da uf d Site.",
en: "What does Team Dogi mean to you? Write us your story — with a bit of luck, DogFather will read it personally and feature it here on the site.",
fr: "Que représente Team Dogi pour toi ? Écris-nous ton histoire — avec un peu de chance, DogFather la lira personnellement et la publiera ici.",
pt: "O que a Team Dogi significa para você? Escreva sua história — com um pouco de sorte, o DogFather vai ler pessoalmente e publicar aqui.",
}, },
st_quote_team: { st_form_name_label: {
de: `„Hier steht bald eine echte Stimme aus dem Team. 🩵"`, de: "Dein Name", "de-CH": "Dis Name",
"de-CH": `„Da staht bald e echti Stimm vom Team. 🩵"`, en: "Your Name", fr: "Ton nom", pt: "Seu nome",
en: `"A real voice from the team will be here soon. 🩵"`,
fr: `« Un vrai témoignage de l'équipe arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real da equipe estará aqui. 🩵"`,
}, },
st_label_team: { st_form_tiktok_label: {
de: "— Team-Mitglied", "de-CH": "— Team-Mitglied", de: "TikTok-Name (optional)", "de-CH": "TikTok-Name (optional)",
en: "— Team Member", fr: "— Membre de l'équipe", pt: "— Membro da equipe", en: "TikTok Username (optional)", fr: "Nom TikTok (facultatif)", pt: "Nome no TikTok (opcional)",
}, },
st_quote_scout: { st_form_nachricht_label: {
de: `„Hier steht bald eine echte Stimme von Scouts &amp; Creatorn. 🩵"`, de: "Deine Nachricht", "de-CH": "Dini Nachricht",
"de-CH": `„Da staht bald e echti Stimm vo Scouts &amp; Creator. 🩵"`, en: "Your Message", fr: "Ton message", pt: "Sua mensagem",
en: `"A real voice from scouts &amp; creators will be here soon. 🩵"`,
fr: `« Un vrai témoignage de scouts et créateurs arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real de scouts e criadores estará aqui. 🩵"`,
}, },
st_label_scout: { st_form_nachricht_placeholder: {
de: "— Scout/Creator", "de-CH": "— Scout/Creator", de: "Erzähl uns, was dir Team Dogi bedeutet …", "de-CH": "Verzell eus, was dir Team Dogi bedütet …",
en: "— Scout/Creator", fr: "— Scout/Créateur", pt: "— Scout/Criador", en: "Tell us what Team Dogi means to you …", fr: "Dis-nous ce que Team Dogi représente pour toi …", pt: "Conte-nos o que a Team Dogi significa para você …",
},
st_form_absenden: {
de: "💙 Nachricht einreichen", "de-CH": "💙 Nachricht ischicke",
en: "💙 Submit Message", fr: "💙 Envoyer le message", pt: "💙 Enviar mensagem",
},
st_form_erfolg: {
de: "Danke dir! Deine Nachricht wurde eingereicht und wird von Dogi persönlich geprüft. 🩵",
"de-CH": "Danke dir! Dini Nachricht isch ischickt worde und wird vo Dogi persönlich gprüeft. 🩵",
en: "Thank you! Your message has been submitted and will be reviewed by Dogi personally. 🩵",
fr: "Merci ! Ton message a été envoyé et sera examiné personnellement par Dogi. 🩵",
pt: "Obrigado! Sua mensagem foi enviada e será revisada pessoalmente pelo Dogi. 🩵",
},
st_echte_titel: {
de: "Das sagt die Community", "de-CH": "Das seit d Community",
en: "What the Community Says", fr: "Ce que dit la communauté", pt: "O que a comunidade diz",
}, },
}; };
+38
View File
@@ -20,6 +20,44 @@ window.I18N_PAGE = {
de: "Zur Dogi TikTok-Seite", "de-CH": "Zur Dogi TikTok-Site", de: "Zur Dogi TikTok-Seite", "de-CH": "Zur Dogi TikTok-Site",
en: "Go to Dogi's TikTok page", fr: "Voir la page TikTok de Dogi", pt: "Ir para a página TikTok do Dogi", en: "Go to Dogi's TikTok page", fr: "Voir la page TikTok de Dogi", pt: "Ir para a página TikTok do Dogi",
}, },
/* Live-Radar (20.08.2026) */
sp_radar_loading: {
de: "Lädt …", "de-CH": "Lädt …",
en: "Loading …", fr: "Chargement …", pt: "Carregando …",
},
sp_radar_offline: {
de: "Nächster Fix-Stream in", "de-CH": "Nächschte Fix-Stream in",
en: "Next fixed stream in", fr: "Prochain live fixe dans", pt: "Próxima live fixa em",
},
sp_radar_countdown_label: {
de: "täglich ab 20 Uhr", "de-CH": "täglich ab 20 Uhr",
en: "daily from 8 PM", fr: "chaque jour à 20h", pt: "diariamente às 20h",
},
sp_radar_live: {
de: "🔴 DogFather ist LIVE", "de-CH": "🔴 DogFather isch LIVE",
en: "🔴 DogFather is LIVE", fr: "🔴 DogFather est EN DIRECT", pt: "🔴 DogFather está AO VIVO",
},
sp_radar_live_generic: {
de: "Jetzt live auf TikTok", "de-CH": "Jetzt live uf TikTok",
en: "Live now on TikTok", fr: "En direct maintenant sur TikTok", pt: "Ao vivo agora no TikTok",
},
sp_radar_watch_btn: {
de: "🔴 Jetzt anschauen", "de-CH": "🔴 Jetzt aluege",
en: "🔴 Watch now", fr: "🔴 Regarder maintenant", pt: "🔴 Assistir agora",
},
sp_radar_hinweis: {
de: "Alle Zeiten in deiner Ortszeit.", "de-CH": "Alli Ziite in dinere Ortsziit.",
en: "All times shown in your local time.", fr: "Tous les horaires affichés dans ton fuseau local.", pt: "Todos os horários na sua hora local.",
},
sp_radar_hinweis_spezial: {
de: "Spezielles Live-Event", "de-CH": "Spezielles Live-Event",
en: "Special live event", fr: "Événement en direct spécial", pt: "Evento ao vivo especial",
},
sp_radar_mehr_erfahren: {
de: "Mehr erfahren →", "de-CH": "Mehr erfahre →",
en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →",
},
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",
+26
View File
@@ -102,4 +102,30 @@ window.I18N_PAGE = {
de: "Zur DogiCrew 🩵", "de-CH": "Zur DogiCrew 🩵", de: "Zur DogiCrew 🩵", "de-CH": "Zur DogiCrew 🩵",
en: "Go to the DogiCrew 🩵", fr: "Aller à la DogiCrew 🩵", pt: "Ir para a DogiCrew 🩵", en: "Go to the DogiCrew 🩵", fr: "Aller à la DogiCrew 🩵", pt: "Ir para a DogiCrew 🩵",
}, },
/* Supporter-Abstimmungen (20.08.2026) */
su_poll_eyebrow: {
de: "🗳️ Aktuelle Abstimmung", "de-CH": "🗳️ Aktueui Abstimmig",
en: "🗳️ Current Poll", fr: "🗳️ Sondage en cours", pt: "🗳️ Enquete atual",
},
su_poll_btn_abstimmen: {
de: "Abstimmen", "de-CH": "Abstimme",
en: "Vote", fr: "Voter", pt: "Votar",
},
su_poll_bitte_waehlen: {
de: "Bitte wähle eine Antwort aus.", "de-CH": "Bitte wähl e Antwort us.",
en: "Please choose an answer.", fr: "Merci de choisir une réponse.", pt: "Por favor, escolha uma resposta.",
},
su_poll_danke: {
de: "Danke, deine Stimme wurde gezählt! 💙", "de-CH": "Danke, dini Stimm isch zellt worde! 💙",
en: "Thanks, your vote was counted! 💙", fr: "Merci, ton vote a été pris en compte ! 💙", pt: "Obrigado, seu voto foi contado! 💙",
},
su_poll_stimmen: {
de: "Stimmen", "de-CH": "Stimme",
en: "votes", fr: "votes", pt: "votos",
},
su_poll_fehler: {
de: "Abstimmen hat leider nicht geklappt. Bitte versuch es nochmal.", "de-CH": "Abstimme het leider nid klappt. Bitte versuech's nomol.",
en: "Voting didn't work. Please try again.", fr: "Le vote a échoué. Merci de réessayer.", pt: "A votação falhou. Por favor, tente novamente.",
},
}; };
+4
View File
@@ -19,6 +19,10 @@ window.I18N_PAGE = {
fr: "L'ossature de Team Dogi — de VanVan à Marina.", fr: "L'ossature de Team Dogi — de VanVan à Marina.",
pt: "A espinha dorsal do Team Dogi — de VanVan a Marina.", pt: "A espinha dorsal do Team Dogi — de VanVan a Marina.",
}, },
tm_btn_tiktok_modigang: {
de: "Zur Modi-Gang TikTok-Seite", "de-CH": "Zur Modi-Gang TikTok-Site",
en: "Go to the Modi Gang TikTok page", fr: "Voir la page TikTok de la Modi-Gang", pt: "Ir para a página TikTok da Modi-Gang",
},
tm_tag_hervorgehoben: { tm_tag_hervorgehoben: {
de: "Hervorgehoben", "de-CH": "Ganz vorne dra", de: "Hervorgehoben", "de-CH": "Ganz vorne dra",
en: "Featured", fr: "En vedette", pt: "Em destaque", en: "Featured", fr: "En vedette", pt: "Em destaque",
+54 -81
View File
@@ -24,7 +24,24 @@ window.I18N_PAGE = {
en: "Date coming soon", fr: "Date à venir", pt: "Data em breve", en: "Date coming soon", fr: "Date à venir", pt: "Data em breve",
}, },
/* ---------- Station 1 — Datum folgt ---------- */ /* ---------- "Wächst noch"-Hinweis (20.08.2026) ---------- */
zr_baustelle_h3: {
de: "Diese Zeitreise wächst noch", "de-CH": "Die Ziitreis wachst no",
en: "This timeline is still growing", fr: "Ce voyage dans le temps continue de grandir", pt: "Esta linha do tempo ainda está crescendo",
},
zr_baustelle_p: {
de: "Wir ergänzen laufend weitere Kapitel, Daten und Momente aus der Geschichte von DogFather &amp; Team Dogi — schau gern öfter mal wieder vorbei.",
"de-CH": "Mir ergänzed laufend wiiteri Kapitel, Date und Momänt us de Gschicht vo DogFather &amp; Team Dogi — schau gärn öppis öfters wieder verbi.",
en: "We're continuously adding more chapters, dates, and moments from the story of DogFather &amp; Team Dogi — feel free to check back again soon.",
fr: "Nous ajoutons en permanence de nouveaux chapitres, dates et moments de l'histoire de DogFather &amp; Team Dogi — n'hésite pas à revenir régulièrement.",
pt: "Estamos adicionando continuamente mais capítulos, datas e momentos da história de DogFather &amp; Team Dogi — volte sempre para conferir.",
},
/* ---------- Station 1 — 24. Oktober 2023 ---------- */
zr_ev1_date: {
de: "24. Oktober 2023", "de-CH": "24. Oktober 2023",
en: "October 24, 2023", fr: "24 octobre 2023", pt: "24 de outubro de 2023",
},
zr_ev1_h3: { zr_ev1_h3: {
de: "Start als Content Creator", "de-CH": "Start als Content Creator", de: "Start als Content Creator", "de-CH": "Start als Content Creator",
en: "Start as a Content Creator", fr: "Débuts en tant que créateur de contenu", pt: "Início como criador de conteúdo", en: "Start as a Content Creator", fr: "Débuts en tant que créateur de contenu", pt: "Início como criador de conteúdo",
@@ -37,7 +54,11 @@ window.I18N_PAGE = {
pt: "DogFather começa sua jornada no TikTok. A partir dos primeiros vídeos e lives, um estilo de conteúdo próprio vai surgindo aos poucos, cheio de humor, personalidade e proximidade real com a comunidade.", pt: "DogFather começa sua jornada no TikTok. A partir dos primeiros vídeos e lives, um estilo de conteúdo próprio vai surgindo aos poucos, cheio de humor, personalidade e proximidade real com a comunidade.",
}, },
/* ---------- Station 2 — Datum folgt ---------- */ /* ---------- Station 2 — Anfang 2025 ---------- */
zr_ev2_date: {
de: "Anfang 2025", "de-CH": "Aafang 2025",
en: "Early 2025", fr: "Début 2025", pt: "Início de 2025",
},
zr_ev2_h3: { zr_ev2_h3: {
de: "Team Dogi entsteht", "de-CH": "Team Dogi entstaht", de: "Team Dogi entsteht", "de-CH": "Team Dogi entstaht",
en: "Team Dogi Is Born", fr: "Naissance de Team Dogi", pt: "Nasce a Team Dogi", en: "Team Dogi Is Born", fr: "Naissance de Team Dogi", pt: "Nasce a Team Dogi",
@@ -84,23 +105,6 @@ window.I18N_PAGE = {
pt: "DogFather encara um desafio especial e transmite ao vivo por 28 horas seguidas. Uma experiência intensa, cheia de diversão, momentos especiais e muita união na comunidade.", pt: "DogFather encara um desafio especial e transmite ao vivo por 28 horas seguidas. Uma experiência intensa, cheia de diversão, momentos especiais e muita união na comunidade.",
}, },
/* ---------- Station 5 — Mai 2026 ---------- */
zr_ev5_date: {
de: "Mai 2026", "de-CH": "Mai 2026",
en: "May 2026", fr: "Mai 2026", pt: "Maio de 2026",
},
zr_ev5_h3: {
de: "Ein neues Kapitel beginnt", "de-CH": "Es neus Kapitel fangt a",
en: "A New Chapter Begins", fr: "Un nouveau chapitre commence", pt: "Um novo capítulo começa",
},
zr_ev5_p: {
de: "DogFather beendet die Zusammenarbeit mit seiner bisherigen Agentur und entscheidet sich dazu, seinen weiteren Weg neu auszurichten. Der Fokus liegt dabei auf persönlicher Weiterentwicklung, neuen Zielen und mehr Eigenständigkeit.",
"de-CH": "DogFather beendet d Zämearbeit mit sinere bisherige Agentur und entscheidet sich derfür, sim wiitere Wäg neu uszrichte. De Fokus liit derbi uf persönlicher Wiiterentwicklig, neue Ziel und meh Eigeständigkeit.",
en: "DogFather ends his collaboration with his previous agency and decides to realign his path. The focus shifts to personal growth, new goals, and greater independence.",
fr: "DogFather met fin à sa collaboration avec son agence précédente et décide de réorienter sa trajectoire. L'accent est désormais mis sur son développement personnel, de nouveaux objectifs et plus d'indépendance.",
pt: "DogFather encerra a parceria com sua antiga agência e decide reorientar seu caminho. O foco passa a ser o desenvolvimento pessoal, novas metas e mais independência.",
},
/* ---------- Station 6 — 2. bis 15. Juni 2026 ---------- */ /* ---------- Station 6 — 2. bis 15. Juni 2026 ---------- */
zr_ev6_date: { zr_ev6_date: {
de: "2. bis 15. Juni 2026", "de-CH": "2. bis 15. Juni 2026", de: "2. bis 15. Juni 2026", "de-CH": "2. bis 15. Juni 2026",
@@ -135,21 +139,21 @@ window.I18N_PAGE = {
pt: "DogFather assume o cargo de gerente na Spicy Media. Além de atuar como streamer, ele passa a acompanhar criadores, descobrir talentos e construir sua própria equipe forte.", pt: "DogFather assume o cargo de gerente na Spicy Media. Além de atuar como streamer, ele passa a acompanhar criadores, descobrir talentos e construir sua própria equipe forte.",
}, },
/* ---------- Station 8 — Juli 2026 ---------- */ /* ---------- Station 21 — 25. Juli 2026 (Meilenstein, Nutzer-Wunsch 21.08.2026) ---------- */
zr_ev8_date: { zr_ev21_date: {
de: "Juli 2026", "de-CH": "Juli 2026", de: "25. Juli 2026", "de-CH": "25. Juli 2026",
en: "July 2026", fr: "Juillet 2026", pt: "Julho de 2026", en: "July 25, 2026", fr: "25 juillet 2026", pt: "25 de julho de 2026",
}, },
zr_ev8_h3: { zr_ev21_h3: {
de: "Das Manager-Team wächst", "de-CH": "Ds Manager-Team wachst", de: "Erstes Fan-Treffen: Team TiliDog", "de-CH": "Erschts Fan-Träffe: Team TiliDog",
en: "The Manager Team Grows", fr: "L'équipe de management s'agrandit", pt: "A equipe de gerência cresce", en: "First Fan Meetup: Team TiliDog", fr: "Première rencontre fans : Team TiliDog", pt: "Primeiro encontro de fãs: Team TiliDog",
}, },
zr_ev8_p: { zr_ev21_p: {
de: "Mit VanVan als rechter Hand und Diene als wichtiger Unterstützung entsteht eine klare Teamstruktur. Gemeinsam werden Creator begleitet, Probleme gelöst und neue Projekte geplant.", de: "In Wuppertal kommt die Community von Dogi &amp; Tili zum allerersten Mal persönlich zusammen – ein richtig mega Tag voller guter Laune. Es ist gleichzeitig etwas ganz Besonderes: das erste Aufeinandertreffen von DogFather und Tili überhaupt.",
"de-CH": "Mit VanVan als rechti Hand und Diene als wichtigi Unterstützig entstaht e klari Teamstruktur. Zäme wärded Creator begleitet, Problem glöst und neui Projekt planet.", "de-CH": "In Wuppertal chunnt d Community vo Dogi &amp; Tili zum allererschte Mal persönlich zäme – en richtig mega Tag voll gueter Luune. Gliichzitig isch es öppis ganz Bsundrigs: s erschte Ufeinanderträffe vo DogFather und Tili überhaupt.",
en: "With VanVan as his right hand and Diene as key support, a clear team structure takes shape. Together, they guide creators, solve problems, and plan new projects.", en: "In Wuppertal, the Dogi &amp; Tili community comes together in person for the very first time – a truly awesome day full of good vibes. It's also something very special: the first time DogFather and Tili ever meet face to face.",
fr: "Avec VanVan comme bras droit et Diene comme soutien essentiel, une structure d'équipe claire se met en place. Ensemble, ils accompagnent les créateurs, résolvent les problèmes et planifient de nouveaux projets.", fr: "À Wuppertal, la communauté de Dogi &amp; Tili se retrouve en personne pour la toute première fois – une journée vraiment géniale et pleine de bonne humeur. C'est aussi un moment très particulier : la toute première rencontre entre DogFather et Tili.",
pt: "Com VanVan como braço direito e Diene como apoio fundamental, surge uma estrutura de equipe clara. Juntos, eles acompanham criadores, resolvem problemas e planejam novos projetos.", pt: "Em Wuppertal, a comunidade de Dogi &amp; Tili se reúne pessoalmente pela primeiríssima vez – um dia realmente incrível e cheio de boas vibrações. É também algo muito especial: o primeiro encontro cara a cara entre DogFather e Tili.",
}, },
/* ---------- Station 9 — 27. Juli bis 2. August 2026 (Meilenstein) ---------- */ /* ---------- Station 9 — 27. Juli bis 2. August 2026 (Meilenstein) ---------- */
@@ -162,11 +166,11 @@ window.I18N_PAGE = {
en: "The Creator Cup", fr: "Le Creator Cup", pt: "O Creator Cup", en: "The Creator Cup", fr: "Le Creator Cup", pt: "O Creator Cup",
}, },
zr_ev9_p: { zr_ev9_p: {
de: "Gemeinsam mit Cidem organisiert DogFather den Creator Cup für die Streamer von Spicy Media. Liveminuten, Teamherzen und Coins werden zu Punkten – verbunden mit besonderen Preisen und einer starken gemeinsamen Aktion.", de: "Gemeinsam mit Cidgem organisiert DogFather den Creator Cup für die Streamer von Spicy Media. Liveminuten, Teamherzen und Coins werden zu Punkten – verbunden mit besonderen Preisen und einer starken gemeinsamen Aktion.",
"de-CH": "Zäme mit Cidem organisiert DogFather de Creator Cup für d Streamer vo Spicy Media. Liveminute, Teamherze und Coins wärded zu Punkt — verbunde mit bsundere Priise und ere starke gmeinsame Aktion.", "de-CH": "Zäme mit Cidgem organisiert DogFather de Creator Cup für d Streamer vo Spicy Media. Liveminute, Teamherze und Coins wärded zu Punkt — verbunde mit bsundere Priise und ere starke gmeinsame Aktion.",
en: "Together with Cidem, DogFather organizes the Creator Cup for Spicy Media's streamers. Live minutes, team hearts, and coins become points — combined with special prizes and a strong joint event.", en: "Together with Cidgem, DogFather organizes the Creator Cup for Spicy Media's streamers. Live minutes, team hearts, and coins become points — combined with special prizes and a strong joint event.",
fr: "Avec Cidem, DogFather organise le Creator Cup pour les streamers de Spicy Media. Minutes de live, cœurs d'équipe et coins deviennent des points, avec des récompenses spéciales et un fort esprit collectif.", fr: "Avec Cidgem, DogFather organise le Creator Cup pour les streamers de Spicy Media. Minutes de live, cœurs d'équipe et coins deviennent des points, avec des récompenses spéciales et un fort esprit collectif.",
pt: "Junto com Cidem, DogFather organiza o Creator Cup para os streamers da Spicy Media. Minutos ao vivo, corações da equipe e coins viram pontos, com prêmios especiais e uma forte ação em conjunto.", pt: "Junto com Cidgem, DogFather organiza o Creator Cup para os streamers da Spicy Media. Minutos ao vivo, corações da equipe e coins viram pontos, com prêmios especiais e uma forte ação em conjunto.",
}, },
/* ---------- Station 10 — 28. Juli 2026 ---------- */ /* ---------- Station 10 — 28. Juli 2026 ---------- */
@@ -220,7 +224,11 @@ window.I18N_PAGE = {
pt: "Começa o planejamento de um site próprio. Ele deve unir pela primeira vez os três grandes universos de DogFather: o streamer DogFather, o gerente na Spicy Media e a marca HasiDog.", pt: "Começa o planejamento de um site próprio. Ele deve unir pela primeira vez os três grandes universos de DogFather: o streamer DogFather, o gerente na Spicy Media e a marca HasiDog.",
}, },
/* ---------- Station 13 — Datum folgt ---------- */ /* ---------- Station 13 — November 2025 ---------- */
zr_ev13_date: {
de: "November 2025", "de-CH": "November 2025",
en: "November 2025", fr: "Novembre 2025", pt: "Novembro de 2025",
},
zr_ev13_h3: { zr_ev13_h3: {
de: `Das Buch „HasiDog &amp; Casper"`, "de-CH": `Ds Buech „HasiDog &amp; Casper"`, de: `Das Buch „HasiDog &amp; Casper"`, "de-CH": `Ds Buech „HasiDog &amp; Casper"`,
en: `The Book "HasiDog &amp; Casper"`, fr: `Le livre « HasiDog &amp; Casper »`, pt: `O livro "HasiDog &amp; Casper"`, en: `The Book "HasiDog &amp; Casper"`, fr: `Le livre « HasiDog &amp; Casper »`, pt: `O livro "HasiDog &amp; Casper"`,
@@ -233,7 +241,11 @@ window.I18N_PAGE = {
pt: "A história especial de DogFather, HasiDog e seu husky Casper se transforma em um projeto de livro próprio. Uma história sobre amizade, família, união e aventuras compartilhadas.", pt: "A história especial de DogFather, HasiDog e seu husky Casper se transforma em um projeto de livro próprio. Uma história sobre amizade, família, união e aventuras compartilhadas.",
}, },
/* ---------- Station 14 — Datum folgt ---------- */ /* ---------- Station 14 — 1. Januar 2026 ---------- */
zr_ev14_date: {
de: "1. Januar 2026", "de-CH": "1. Januar 2026",
en: "January 1, 2026", fr: "1er janvier 2026", pt: "1º de janeiro de 2026",
},
zr_ev14_h3: { zr_ev14_h3: {
de: "HasiDog startet auf Spotify", "de-CH": "HasiDog startet uf Spotify", de: "HasiDog startet auf Spotify", "de-CH": "HasiDog startet uf Spotify",
en: "HasiDog Launches on Spotify", fr: "HasiDog arrive sur Spotify", pt: "HasiDog estreia no Spotify", en: "HasiDog Launches on Spotify", fr: "HasiDog arrive sur Spotify", pt: "HasiDog estreia no Spotify",
@@ -259,49 +271,10 @@ window.I18N_PAGE = {
pt: "Começa uma colaboração própria de produtos em torno da VanVan. Estão previstos produtos especiais que refletem sua personalidade e seu papel importante dentro da Team Dogi.", pt: "Começa uma colaboração própria de produtos em torno da VanVan. Estão previstos produtos especiais que refletem sua personalidade e seu papel importante dentro da Team Dogi.",
}, },
/* ---------- Station 16 — Datum folgt ---------- */ /* ---------- Station 19 — 21. August 2026, 21 Uhr (Meilenstein) ---------- */
zr_ev16_h3: {
de: "Die ersten betreuten Creator", "de-CH": "Die erste betreute Creator",
en: "The First Supported Creators", fr: "Les premiers créateurs accompagnés", pt: "Os primeiros criadores acompanhados",
},
zr_ev16_p: {
de: "Die ersten Creator schließen sich dem Team von Manager DogFather an. Sie erhalten persönliche Unterstützung, Hilfe bei Problemen, Profil-Checks, Schulungen und eine langfristige Begleitung auf TikTok.",
"de-CH": "Die erste Creator schliessed sich em Team vom Manager DogFather a. Sie überchömed persönlichi Unterstützig, Hilf bi Problem, Profil-Checks, Schulige und e langfristigi Begleitig uf TikTok.",
en: "The first creators join Manager DogFather's team. They receive personal support, help with problems, profile checks, training, and long-term guidance on TikTok.",
fr: "Les premiers créateurs rejoignent l'équipe du manager DogFather. Ils bénéficient d'un accompagnement personnalisé, d'aide en cas de problème, de vérifications de profil, de formations et d'un suivi durable sur TikTok.",
pt: "Os primeiros criadores se juntam à equipe do gerente DogFather. Eles recebem apoio pessoal, ajuda com problemas, análises de perfil, treinamentos e acompanhamento de longo prazo no TikTok.",
},
/* ---------- Station 17 — Datum folgt ---------- */
zr_ev17_h3: {
de: "Weitere Scouts und Manager kommen hinzu", "de-CH": "Wiiteri Scouts und Manager chömed dezue",
en: "More Scouts and Managers Join", fr: "Plus de scouts et de managers rejoignent l'équipe", pt: "Mais scouts e gerentes se juntam",
},
zr_ev17_p: {
de: "Das Team soll langfristig weiter wachsen. Engagierte Scouts erhalten die Möglichkeit, sich über verschiedene Entwicklungsstufen bis hin zum eigenen Manager weiterzuentwickeln.",
"de-CH": "Ds Team söll langfristig wiiter wachse. Engagierti Scouts überchömed d Möglichkeit, sich über verschideni Entwickligsstufe bis zum eigete Manager wiiterzentwickle.",
en: "The team is set to keep growing long-term. Dedicated scouts get the chance to develop through various stages all the way up to becoming a manager themselves.",
fr: "L'équipe est destinée à continuer de grandir sur le long terme. Les scouts engagés ont la possibilité d'évoluer à travers différents niveaux, jusqu'à devenir eux-mêmes managers.",
pt: "A equipe deve continuar crescendo a longo prazo. Scouts dedicados ganham a chance de evoluir por diferentes níveis até se tornarem gerentes.",
},
/* ---------- Station 18 — Datum folgt ---------- */
zr_ev18_h3: {
de: "Das erste Community-Treffen", "de-CH": "Ds erste Community-Träffe",
en: "The First Community Meetup", fr: "La première rencontre communautaire", pt: "O primeiro encontro da comunidade",
},
zr_ev18_p: {
de: "Team Dogi begegnet sich nicht mehr nur im Livestream oder auf Discord. Beim gemeinsamen Treffen entstehen echte Erinnerungen, die zeigen, dass diese Community weit mehr als nur Zahlen und Likes ist.",
"de-CH": "Team Dogi begägnet sich nid nur im Livestream oder uf Discord. Bim gmeinsame Träffe entstönd echti Erinnerige, wo zeiged, dass die Community wit meh isch als nume Zahle und Likes.",
en: "Team Dogi no longer just meets in livestreams or on Discord. At the shared meetup, real memories are made — proof that this community is far more than just numbers and likes.",
fr: "Team Dogi ne se retrouve plus seulement en live ou sur Discord. Lors de la rencontre commune naissent de vrais souvenirs, preuve que cette communauté est bien plus que des chiffres et des likes.",
pt: "A Team Dogi não se encontra mais só em lives ou no Discord. No encontro presencial nascem memórias reais, mostrando que essa comunidade é muito mais do que números e curtidas.",
},
/* ---------- Station 19 — 2026 – Datum folgt (Meilenstein) ---------- */
zr_ev19_date: { zr_ev19_date: {
de: "2026 – Datum folgt", "de-CH": "2026 – Datum chunnt no", de: "21. August 2026, 21 Uhr", "de-CH": "21. Auguscht 2026, 21 Uhr",
en: "2026 – date coming soon", fr: "2026 – date à venir", pt: "2026 – data em breve", en: "August 21, 2026, 9 PM", fr: "21 août 2026, 21 h", pt: "21 de agosto de 2026, 21h",
}, },
zr_ev19_h3: { zr_ev19_h3: {
de: "DOGFATHER UNIVERSE geht online", "de-CH": "DOGFATHER UNIVERSE gaht online", de: "DOGFATHER UNIVERSE geht online", "de-CH": "DOGFATHER UNIVERSE gaht online",
+247 -13
View File
@@ -76,15 +76,22 @@
const I18N_CORE = { const I18N_CORE = {
nav_start: { de: "Start", "de-CH": "Start", en: "Home", fr: "Accueil", pt: "Início" }, nav_start: { de: "Start", "de-CH": "Start", en: "Home", fr: "Accueil", pt: "Início" },
nav_dogfather: { de: "DogFather", "de-CH": "DogFather", en: "DogFather", fr: "DogFather", pt: "DogFather" }, /* Umbenennungen 20.08.2026 (Nutzer-Wunsch): Kategorie "DogFather" -> "Filipe",
Kategorie "HasiDog" -> "Dogi&Hasi". Die Schlüsselnamen (nav_dogfather...)
bleiben bewusst unverändert — sie sind reine interne Bezeichner und werden
an mehreren Stellen (Nav, Fußzeile, Farbzuordnung) referenziert; ein
Umbenennen würde nur Fehlerquellen schaffen, ohne sichtbaren Nutzen. */
nav_dogfather: { de: "Filipe", "de-CH": "Filipe", en: "Filipe", fr: "Filipe", pt: "Filipe" },
nav_dogfather_person: { de: "Die Person", "de-CH": "D Person", en: "The Person", fr: "La personne", pt: "A pessoa" }, nav_dogfather_person: { de: "Die Person", "de-CH": "D Person", en: "The Person", fr: "La personne", pt: "A pessoa" },
nav_dogfather_streamer: { de: "Streamer", "de-CH": "Streamer", en: "Streamer", fr: "Streamer", pt: "Streamer" }, /* Sitzt seit 20.08.2026 unter "Dogi&Hasi" (nicht mehr unter "Filipe") und heißt
dort "DogFather" statt "Streamer" — zeigt weiterhin auf streamer.html. */
nav_dogfather_streamer: { de: "DogFather", "de-CH": "DogFather", en: "DogFather", fr: "DogFather", pt: "DogFather" },
nav_dogfather_casper: { de: "Casper", "de-CH": "Casper", en: "Casper", fr: "Casper", pt: "Casper" }, nav_dogfather_casper: { de: "Casper", "de-CH": "Casper", en: "Casper", fr: "Casper", pt: "Casper" },
nav_dogfather_vanvan: { de: "VanVan", "de-CH": "VanVan", en: "VanVan", fr: "VanVan", pt: "VanVan" }, nav_dogfather_vanvan: { de: "VanVan", "de-CH": "VanVan", en: "VanVan", fr: "VanVan", pt: "VanVan" },
nav_dogfather_zeitreise: { de: "Zeitreise", "de-CH": "Zytreis", en: "Timeline", fr: "Chronologie", pt: "Linha do tempo" }, nav_dogfather_zeitreise: { de: "Zeitreise", "de-CH": "Zytreis", en: "Timeline", fr: "Chronologie", pt: "Linha do tempo" },
nav_dogfather_ttmuddi: { de: "TT-Muddi", "de-CH": "TT-Muddi", en: "TT-Muddi", fr: "TT-Muddi", pt: "TT-Muddi" }, nav_dogfather_ttmuddi: { de: "TT-Muddi", "de-CH": "TT-Muddi", en: "TT-Muddi", fr: "TT-Muddi", pt: "TT-Muddi" },
nav_hasidog: { de: "HasiDog", "de-CH": "HasiDog", en: "HasiDog", fr: "HasiDog", pt: "HasiDog" }, nav_hasidog: { de: "Dogi&Hasi", "de-CH": "Dogi&Hasi", en: "Dogi&Hasi", fr: "Dogi&Hasi", pt: "Dogi&Hasi" },
nav_hasidog_welt: { de: "HasiDog-Welt", "de-CH": "HasiDog-Wält", en: "HasiDog World", fr: "Monde HasiDog", pt: "Mundo HasiDog" }, nav_hasidog_welt: { de: "HasiDog", "de-CH": "HasiDog", en: "HasiDog", fr: "HasiDog", pt: "HasiDog" },
nav_hasidog_modi: { de: "Modi-Team", "de-CH": "Modi-Team", en: "Modi-Team", fr: "Modi-Team", pt: "Modi-Team" }, nav_hasidog_modi: { de: "Modi-Team", "de-CH": "Modi-Team", en: "Modi-Team", fr: "Modi-Team", pt: "Modi-Team" },
nav_hasidog_community: { de: "Community", "de-CH": "Community", en: "Community", fr: "Communauté", pt: "Comunidade" }, nav_hasidog_community: { de: "Community", "de-CH": "Community", en: "Community", fr: "Communauté", pt: "Comunidade" },
nav_hasidog_stimmen: { de: "Stimmen", "de-CH": "Stimme", en: "Voices", fr: "Témoignages", pt: "Depoimentos" }, nav_hasidog_stimmen: { de: "Stimmen", "de-CH": "Stimme", en: "Voices", fr: "Témoignages", pt: "Depoimentos" },
@@ -104,6 +111,11 @@
nav_cta_abo_anonym: { de: "DogiCrew 👑", "de-CH": "DogiCrew 👑", en: "DogiCrew 👑", fr: "DogiCrew 👑", pt: "DogiCrew 👑" }, nav_cta_abo_anonym: { de: "DogiCrew 👑", "de-CH": "DogiCrew 👑", en: "DogiCrew 👑", fr: "DogiCrew 👑", pt: "DogiCrew 👑" },
nav_cta_abo_aktivieren: { de: "Abo aktivieren 👑", "de-CH": "Abo aktiviere 👑", en: "Activate Subscription 👑", fr: "Activer l'abonnement 👑", pt: "Ativar assinatura 👑" }, nav_cta_abo_aktivieren: { de: "Abo aktivieren 👑", "de-CH": "Abo aktiviere 👑", en: "Activate Subscription 👑", fr: "Activer l'abonnement 👑", pt: "Ativar assinatura 👑" },
nav_cta_abo_bereich: { de: "Supporter-Bereich 👑", "de-CH": "Supporter-Bereich 👑", en: "Supporter Area 👑", fr: "Espace Supporter 👑", pt: "Área do Apoiador 👑" }, nav_cta_abo_bereich: { de: "Supporter-Bereich 👑", "de-CH": "Supporter-Bereich 👑", en: "Supporter Area 👑", fr: "Espace Supporter 👑", pt: "Área do Apoiador 👑" },
/* Kleines Schimmer-Badge auf dem Abo-Knopf, solange die Registrierung
noch gesperrt ist (20.08.2026, Nutzer-Wunsch) — bewusst ein kurzes
Wort statt "Coming soon…", weil in der schmalen Nav-Pille (siehe
Mobil-Menü-Fix vom selben Tag) kaum Platz ist. */
nav_cta_abo_soon: { de: "Bald", "de-CH": "Bald", en: "Soon", fr: "Bientôt", pt: "Em breve" },
nav_toggle_aria: { de: "Menü öffnen", "de-CH": "Menü uftue", en: "Open menu", fr: "Ouvrir le menu", pt: "Abrir menu" }, nav_toggle_aria: { de: "Menü öffnen", "de-CH": "Menü uftue", en: "Open menu", fr: "Ouvrir le menu", pt: "Abrir menu" },
sprache_aria: { de: "Sprache wählen", "de-CH": "Sprach uswähle", en: "Choose language", fr: "Choisir la langue", pt: "Escolher idioma" }, sprache_aria: { de: "Sprache wählen", "de-CH": "Sprach uswähle", en: "Choose language", fr: "Choisir la langue", pt: "Escolher idioma" },
brand_tagline: { brand_tagline: {
@@ -120,7 +132,9 @@
fr: "Streamer. Communauté. HasiDog. Manager. Plus forts ensemble.", fr: "Streamer. Communauté. HasiDog. Manager. Plus forts ensemble.",
pt: "Streamer. Comunidade. HasiDog. Manager. Mais fortes juntos.", pt: "Streamer. Comunidade. HasiDog. Manager. Mais fortes juntos.",
}, },
footer_h4_hasidog_manager: { de: "HasiDog & Manager", "de-CH": "HasiDog & Manager", en: "HasiDog & Manager", fr: "HasiDog et Manager", pt: "HasiDog e Manager" }, /* Fußzeilen-Überschrift zieht mit der Kategorie-Umbenennung mit (20.08.2026):
"HasiDog & Manager" -> "Dogi&Hasi & Manager". */
footer_h4_hasidog_manager: { de: "Dogi&Hasi & Manager", "de-CH": "Dogi&Hasi & Manager", en: "Dogi&Hasi & Manager", fr: "Dogi&Hasi et Manager", pt: "Dogi&Hasi e Manager" },
footer_h4_mehr: { de: "Mehr", "de-CH": "Meh", en: "More", fr: "Plus", pt: "Mais" }, footer_h4_mehr: { de: "Mehr", "de-CH": "Meh", en: "More", fr: "Plus", pt: "Mais" },
footer_scouts: { de: "Scouts", "de-CH": "Scouts", en: "Scouts", fr: "Scouts", pt: "Scouts" }, footer_scouts: { de: "Scouts", "de-CH": "Scouts", en: "Scouts", fr: "Scouts", pt: "Scouts" },
footer_alle_links: { de: "Alle Links", "de-CH": "Alli Links", en: "All links", fr: "Tous les liens", pt: "Todos os links" }, footer_alle_links: { de: "Alle Links", "de-CH": "Alli Links", en: "All links", fr: "Tous les liens", pt: "Todos os links" },
@@ -133,6 +147,7 @@
pt: "© {jahr} DogFather Universe. Team Dogi é família 🩵", pt: "© {jahr} DogFather Universe. Team Dogi é família 🩵",
}, },
footer_impressum: { de: "Impressum & Datenschutz", "de-CH": "Impressum & Datenschutz", en: "Legal notice & privacy", fr: "Mentions légales et confidentialité", pt: "Aviso legal e privacidade" }, footer_impressum: { de: "Impressum & Datenschutz", "de-CH": "Impressum & Datenschutz", en: "Legal notice & privacy", fr: "Mentions légales et confidentialité", pt: "Aviso legal e privacidade" },
footer_app_installieren: { de: "Als App installieren", "de-CH": "Als App installiere", en: "Install as app", fr: "Installer comme application", pt: "Instalar como aplicativo" },
music_title: { de: "Musik von Team Dogi 🎵", "de-CH": "Musig vo Team Dogi 🎵", en: "Music from Team Dogi 🎵", fr: "Musique de Team Dogi 🎵", pt: "Música do Team Dogi 🎵" }, music_title: { de: "Musik von Team Dogi 🎵", "de-CH": "Musig vo Team Dogi 🎵", en: "Music from Team Dogi 🎵", fr: "Musique de Team Dogi 🎵", pt: "Música do Team Dogi 🎵" },
music_close_aria: { de: "Musik-Feld schließen", "de-CH": "Musig-Feld zuemache", en: "Close music panel", fr: "Fermer le panneau musique", pt: "Fechar painel de música" }, music_close_aria: { de: "Musik-Feld schließen", "de-CH": "Musig-Feld zuemache", en: "Close music panel", fr: "Fermer le panneau musique", pt: "Fechar painel de música" },
music_intro: { music_intro: {
@@ -184,6 +199,22 @@
if (text) el.setAttribute(attr, text); if (text) el.setAttribute(attr, text);
}); });
}); });
// 21.08.2026 (Nutzer-Wunsch: "ich will ich titel und namen von
// kategorien und seiten ändern können in der verwaltung"): admin-
// editierbare Text-Overrides (siehe window.dogiSiteTexteLaden() weiter
// unten). Läuft bewusst NACH den normalen data-i18n-Standardtexten
// oben, damit ein gesetzter Override sie überschreiben kann. Fehlt ein
// Override (Normalfall, noch nichts in der Verwaltung eingetragen),
// bleibt einfach der eingebaute Standardtext stehen -- diese Funktion
// hier macht dann schlicht nichts.
const siteTexte = window.__siteTexteCache || {};
document.querySelectorAll("[data-site-text-key]").forEach((el) => {
const eintrag = siteTexte[el.getAttribute("data-site-text-key")];
if (!eintrag) return;
const text = eintrag[sprache] || eintrag.de;
if (text) el.innerHTML = text;
});
} }
/* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */ /* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */
@@ -193,7 +224,6 @@
key: "nav_dogfather", key: "nav_dogfather",
children: [ children: [
{ key: "nav_dogfather_person", href: "dogfather.html" }, { key: "nav_dogfather_person", href: "dogfather.html" },
{ key: "nav_dogfather_streamer", href: "streamer.html" },
{ key: "nav_dogfather_casper", href: "casper.html" }, { key: "nav_dogfather_casper", href: "casper.html" },
{ key: "nav_dogfather_vanvan", href: "vanvan.html" }, { key: "nav_dogfather_vanvan", href: "vanvan.html" },
{ key: "nav_dogfather_ttmuddi", href: "tt-muddi.html" }, { key: "nav_dogfather_ttmuddi", href: "tt-muddi.html" },
@@ -203,6 +233,10 @@
{ {
key: "nav_hasidog", key: "nav_hasidog",
children: [ children: [
/* "DogFather" (streamer.html) hierher verschoben — Nutzer-Wunsch
20.08.2026: gehört unter "Dogi&Hasi" und dort GANZ NACH OBEN,
direkt über "HasiDog". */
{ key: "nav_dogfather_streamer", href: "streamer.html" },
{ key: "nav_hasidog_welt", href: "hasidog.html" }, { key: "nav_hasidog_welt", href: "hasidog.html" },
{ key: "nav_hasidog_modi", href: "team-modis.html" }, { key: "nav_hasidog_modi", href: "team-modis.html" },
{ key: "nav_hasidog_community", href: "community.html" }, { key: "nav_hasidog_community", href: "community.html" },
@@ -454,6 +488,17 @@
return `${escapeHtml(kuerzen(sauber, 18))} 👑`; return `${escapeHtml(kuerzen(sauber, 18))} 👑`;
} }
/* Kleines Schimmer-Badge "Bald", solange die eigentliche Registrierung auf
abonnieren.html gesperrt ist (20.08.2026, siehe .btn-gold-locked dort).
Nur auf den drei Zielen, die dort ankommen — NICHT auf supporter.html
(echter, bereits funktionierender Bereich für Dogi/VanVan als Test-
Supporter), sonst würde es fälschlich so wirken, als sei der ganze
Supporter-Bereich gesperrt. Reine Wiederverwendung von .btn-silver-text/
-spark (siehe main.css) in kleinerem Maßstab, kein neuer Mechanismus. */
function soonBadge() {
return `<span class="nav-cta-soon"><span class="btn-silver-spark" aria-hidden="true">✨</span><span class="btn-silver-text">${t("nav_cta_abo_soon")}</span></span>`;
}
function renderAboButton() { function renderAboButton() {
const vorschau = getVorschauModus(); const vorschau = getVorschauModus();
if (vorschau === "abonnent") { if (vorschau === "abonnent") {
@@ -469,20 +514,20 @@
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(echterName || vorschauAnzeigename())}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(echterName || vorschauAnzeigename())}</span></a>`;
} }
if (vorschau === "nicht-abonnent") { if (vorschau === "nicht-abonnent") {
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span>${soonBadge()}</a>`;
} }
const token = localStorage.getItem("supporterToken"); const token = localStorage.getItem("supporterToken");
const status = localStorage.getItem("supporterSubStatus"); 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></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); const hatBereich = ["active", "pending", "suspended", "cancelled"].includes(status);
if (hatBereich) { if (hatBereich) {
const name = localStorage.getItem("supporterDisplayName"); const name = localStorage.getItem("supporterDisplayName");
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`;
} }
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_aktivieren")}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_aktivieren")}</span>${soonBadge()}</a>`;
} }
function renderHeader() { function renderHeader() {
@@ -524,6 +569,24 @@
}); });
}); });
/* Bugfix 20.08.2026 (Nutzer-Report per Screenshot): ein per Klick
geöffnetes Kategorie-Menü blieb offen stehen, bis irgendwo anders
hingeklickt wurde — verlässt man es einfach nur mit der Maus (ohne
zu klicken), sollte es sich von selbst schließen, genau wie das
reine Hover-Aufklappen (siehe main.css @media (hover:hover)) es
nahelegt. Nur auf echten Maus-Geräten (nicht Touch/Tablet, dort
gibt es kein "Verlassen mit der Maus", nur echte Klicks). */
const hatEchteMaus = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (hatEchteMaus) {
el.querySelectorAll(".nav-group").forEach((group) => {
group.addEventListener("mouseleave", () => {
if (!group.classList.contains("open")) return;
group.classList.remove("open");
group.querySelector("button")?.setAttribute("aria-expanded", "false");
});
});
}
el.querySelectorAll(".sprach-schalter [data-sprache]").forEach((link) => { el.querySelectorAll(".sprach-schalter [data-sprache]").forEach((link) => {
link.addEventListener("click", (e) => { link.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
@@ -554,12 +617,12 @@
<p class="small" style="margin-top:.8rem;max-width:32ch;"> <p class="small" style="margin-top:.8rem;max-width:32ch;">
${t("footer_tagline")} ${t("footer_tagline")}
</p> </p>
<button type="button" id="pwa-install-btn" class="btn btn-outline" style="display:none;margin-top:1rem;">📲 ${t("footer_app_installieren")}</button>
</div> </div>
<div> <div>
<h4>DogFather</h4> <h4>${t("nav_dogfather")}</h4>
<ul> <ul>
<li><a href="dogfather.html">${t("nav_dogfather_person")}</a></li> <li><a href="dogfather.html">${t("nav_dogfather_person")}</a></li>
<li><a href="streamer.html">${t("nav_dogfather_streamer")}</a></li>
<li><a href="casper.html">${t("nav_dogfather_casper")}</a></li> <li><a href="casper.html">${t("nav_dogfather_casper")}</a></li>
<li><a href="vanvan.html">${t("nav_dogfather_vanvan")}</a></li> <li><a href="vanvan.html">${t("nav_dogfather_vanvan")}</a></li>
<li><a href="tt-muddi.html">${t("nav_dogfather_ttmuddi")}</a></li> <li><a href="tt-muddi.html">${t("nav_dogfather_ttmuddi")}</a></li>
@@ -569,6 +632,10 @@
<div> <div>
<h4>${t("footer_h4_hasidog_manager")}</h4> <h4>${t("footer_h4_hasidog_manager")}</h4>
<ul> <ul>
<!-- "DogFather"/streamer.html steht seit 20.08.2026 auch in der
Fußzeile in dieser Spalte (und ganz oben), damit sie dieselbe
Struktur zeigt wie die Navigation darüber. -->
<li><a href="streamer.html">${t("nav_dogfather_streamer")}</a></li>
<li><a href="hasidog.html">${t("nav_hasidog_welt")}</a></li> <li><a href="hasidog.html">${t("nav_hasidog_welt")}</a></li>
<li><a href="team-modis.html">${t("nav_hasidog_modi")}</a></li> <li><a href="team-modis.html">${t("nav_hasidog_modi")}</a></li>
<li><a href="community.html">${t("nav_hasidog_community")}</a></li> <li><a href="community.html">${t("nav_hasidog_community")}</a></li>
@@ -874,7 +941,22 @@
// erneut laufen, sonst bleibt window.I18N_PAGE nach einem // erneut laufen, sonst bleibt window.I18N_PAGE nach einem
// Zwischenstopp auf einer anderen Seite (die es zurückgesetzt hat) // Zwischenstopp auf einer anderen Seite (die es zurückgesetzt hat)
// fälschlich leer, wenn man z.B. zur Startseite zurückkehrt. // fälschlich leer, wenn man z.B. zur Startseite zurückkehrt.
const istSprachDatei = /\/i18n-[^/]+\.js$/.test(src); //
// BUGFIX 21.08.2026 (Nutzer-Report: rohe Übersetzungs-Schlüssel wie
// "ts_profil_ansehen" sichtbar, aber NUR nachdem man auf einen Link
// geklickt und dann "Zurück" gedrückt hat): das Muster endete bisher
// auf ".js$" (striktes String-Ende). Seit die Cache-Busting-Version
// ("?v=20260821...") an JEDE Skript-URL angehängt wird, endet der
// echte src-Wert aber nie mehr auf ".js", sondern auf ".js?v=...".
// Der Test schlug dadurch für JEDE i18n-Datei fehl -> istSprachDatei
// war immer false -> die i18n-Datei wurde wie eine normale, schon
// geladene Datei übersprungen und NIE erneut ausgeführt, sobald man
// per Klick auf eine andere Seite und wieder zurück navigiert ist.
// window.I18N_PAGE blieb dadurch dauerhaft auf null (von der
// Zwischenseite zurückgesetzt) stehen -- jede Übersetzung fiel auf
// den rohen Schlüsselnamen zurück. Fix: ".js" darf jetzt auch von
// einem "?..." gefolgt werden, nicht nur vom exakten String-Ende.
const istSprachDatei = /\/i18n-[^/]+\.js(\?.*)?$/.test(src);
// Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei): // Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei):
// nicht erneut ausführen — sonst gäbe es doppelte Deklarationen. // nicht erneut ausführen — sonst gäbe es doppelte Deklarationen.
@@ -986,6 +1068,7 @@
await skripteUebernehmen(dok); await skripteUebernehmen(dok);
applyTranslations(); // seiteneigene data-i18n-Elemente der neuen Seite übersetzen applyTranslations(); // seiteneigene data-i18n-Elemente der neuen Seite übersetzen
if (window.dogiSiteTexteLaden) window.dogiSiteTexteLaden(); // Kategorie-/Seitentitel-Overrides der neuen Seite
} }
function nahtloseNavigationAktivieren() { function nahtloseNavigationAktivieren() {
@@ -1039,10 +1122,25 @@
renderFooter(); renderFooter();
renderFog(); renderFog();
renderMusicWidget(); renderMusicWidget();
renderVorschauSchalter(); // Bugfix/Nutzer-Wunsch 20.08.2026, vor dem morgigen öffentlichen Start:
// "die leute sollen das ja nicht selbst auswählen können". Der Schalter
// erkennt Dogi/VanVan bisher nur über das rein client-seitig lesbare
// Cookie dogi_role (bewusst NICHT das echte, HttpOnly-geschützte
// Sicherheits-Cookie, siehe Kommentar bei getVorschauRolle() oben) —
// technisch könnte sich also jede Besucherin/jeder Besucher dieses
// Cookie selbst per Browser-Konsole setzen und den Schalter auslösen.
// Echte Daten wären dadurch nie einsehbar (der Schalter zeigt nur
// Beispieldaten für den eigenen Browser), aber "soll weg für die
// Öffentlichkeit" heißt hier bewusst: ganz weg, nicht nur "sicherer".
// renderVorschauSchalter() bleibt unangetastet erhalten (funktioniert
// exakt wie vorher), nur DIESER Aufruf beim Seitenaufbau ist deaktiviert
// — für ein künftiges internes Testen kann diese Zeile jederzeit wieder
// aktiviert werden.
// renderVorschauSchalter();
musikKnopfFarbenAnpassen(); musikKnopfFarbenAnpassen();
nahtloseNavigationAktivieren(); nahtloseNavigationAktivieren();
applyTranslations(); // seiteneigene data-i18n-Elemente (z.B. Startseite) übersetzen applyTranslations(); // seiteneigene data-i18n-Elemente (z.B. Startseite) übersetzen
if (window.dogiSiteTexteLaden) window.dogiSiteTexteLaden(); // Kategorie-/Seitentitel-Overrides (siehe Verwaltung)
}); });
/* Kleine öffentliche Schnittstelle für seiteneigene Inline-Skripte /* Kleine öffentliche Schnittstelle für seiteneigene Inline-Skripte
@@ -1074,6 +1172,99 @@
return feld[sprache] || feld.de || ""; return feld[sprache] || feld.de || "";
}; };
/* =====================================================================
dogiTeamLaden() — 21.08.2026, Nutzer-Wunsch: "überall wo Modis sind
oder Scouts oder Manager, oder Creator, will ich dass ich die easy
über meine Verwaltungsseite hinzufüge und die automatisch in der
Website hinzugefügt werden ... auch mit Fotos und TikTok Link."
Holt die über die Verwaltung angelegten Team-Mitglieder einer
Kategorie ("modi" | "scout" | "creator" | "manager") vom Server und
bringt sie in GENAU dieselbe Objektform wie die von Hand gepflegten
Einträge in data-modis.js/data-scouts.js/data-creator.js/
data-manager.js (name/slug/role/photo/bio/quote/social/...) — die
aufrufende Seite hängt das Ergebnis einfach hinten an ihr bestehendes
window.MODIS/SCOUTS/CREATORS/MANAGERS-Array an und ruft ihre eigene,
unveränderte Render-Funktion erneut auf. Kein kategorie-Parameter
liefert ALLE Kategorien zusammen (genutzt von profil.html, das vorab
nicht weiß, zu welcher Kategorie ein slug gehört).
Schlägt die Anfrage fehl (Server nicht erreichbar, Netz weg): leeres
Array statt Fehler -- die von Hand gepflegten Einträge bleiben davon
komplett unberührt sichtbar, nur admin-gepflegte kommen dann halt
(vorübergehend) nicht hinzu. */
const TEAM_API_BASIS = "https://postfach.dogfather-universe.com";
window.dogiTeamLaden = async function (kategorie) {
try {
const url = kategorie ? `${TEAM_API_BASIS}/team?kategorie=${encodeURIComponent(kategorie)}` : `${TEAM_API_BASIS}/team`;
const res = await fetch(url, { cache: "no-store" });
const data = await res.json();
if (!data || !data.ok || !Array.isArray(data.mitglieder)) return [];
return data.mitglieder.map((m) => ({
name: m.name,
slug: m.slug,
kategorie: m.kategorie,
role: m.rolle || "",
photo: m.foto || "",
avatar: m.foto || "",
// 21.08.2026 (Runde 2, "wenn ich eine änderung mache ist es auch
// für jeden"): volle Feld-Parität mit den von Hand gepflegten
// Profilen, damit ein importiertes/bearbeitetes Profil genau so
// reichhaltig sein kann wie die schon bestehenden.
intro: m.intro || "",
bio: m.bio || "",
bioHtml: m.bioHtml || "",
quote: m.zitat || "",
social: m.tiktok ? { tiktok: m.tiktok } : {},
status: "aktiv", // /team liefert ohnehin nur aktive Mitglieder
rank: m.rang || undefined,
rankLabel: m.rangLabel || undefined,
extraCta: m.extraCta || undefined,
// Markiert admin-gepflegte Einträge (z.B. für profil.html, das
// sonst nicht zwischen fest einprogrammierten und dynamischen
// Personen unterscheiden könnte).
istAdminGepflegt: true,
}));
} catch {
return [];
}
};
/* 21.08.2026 (Runde 2, Nutzer-Wunsch: "wenn ich eine änderung mache ist
es auch für jeden für die die schon da sind und die neuen"): fügt eine
per dogiTeamLaden() geladene Liste NICHT einfach hinten an die von Hand
gepflegte Liste an, sondern ÜBERSCHREIBT darin jeden Eintrag mit
demselben slug. So zeigt die Seite nach einem Import + einer
Bearbeitung in der Verwaltung automatisch den aktuellen (DB-)Stand
statt der alten, einprogrammierten Version -- ist das Backend mal
nicht erreichbar, bleibt wenigstens die statische Version sichtbar
(nichts verschwindet einfach). */
window.dogiTeamZusammenfuehren = function (statische, neue) {
if (!neue || !neue.length) return statische;
const neueSlugs = new Set(neue.map((n) => n.slug));
return [...statische.filter((s) => !neueSlugs.has(s.slug)), ...neue];
};
/* 21.08.2026 (Nutzer-Wunsch: "ich will ich titel und namen von
kategorien und seiten ändern können in der verwaltung"): lädt einmalig
alle in der Verwaltung gesetzten Text-Overrides (siehe
server-internal/routes/site-texts.js) und ruft applyTranslations()
danach erneut auf, damit gesetzte Overrides die eingebauten
Standardtexte sofort ersetzen. Ohne Overrides (Normalfall) ändert sich
einfach nichts -- die Seite zeigt weiterhin ihren gewohnten Text. */
window.dogiSiteTexteLaden = async function () {
try {
const res = await fetch(`${TEAM_API_BASIS}/site-texts`, { cache: "no-store" });
const data = await res.json();
if (data && data.ok) {
window.__siteTexteCache = data.texte || {};
applyTranslations();
}
} catch {
/* kein Override verfügbar -> eingebaute Standardtexte bleiben stehen */
}
};
/* =================================================================== /* ===================================================================
PWA: Service-Worker-Registrierung PWA: Service-Worker-Registrierung
Voraussetzung dafür, dass Chrome/Edge auf dem Desktop "App Voraussetzung dafür, dass Chrome/Edge auf dem Desktop "App
@@ -1115,4 +1306,47 @@
appleTitel.content = "DogFather"; appleTitel.content = "DogFather";
document.head.appendChild(appleTitel); document.head.appendChild(appleTitel);
} }
/* ===================================================================
PWA: sichtbarer "App installieren"-Knopf (20.08.2026)
Nutzer-Wunsch: "ich kann sie immer noch nicht runter laden" — reine
Installierbarkeit über Manifest+Service-Worker reicht nicht, wenn
niemand weiß, WO im Browser man das anklickt (verstecktes
Adressleisten-Symbol/3-Punkte-Menü). Jede Seite mit einem Element
#pwa-install-btn (siehe renderFooter() oben + verwaltung.html) bekommt
jetzt einen echten, sichtbaren Knopf, der den nativen Install-Dialog
direkt auslöst — funktioniert pro Seite mit GENAU dem Manifest, das
diese Seite selbst verlinkt (index.html -> manifest.json,
verwaltung.html -> manifest-verwaltung.json), ganz ohne Sonderfall-Code
hier. Zeigt sich NUR, wenn der Browser das überhaupt unterstützt UND
die Seite nicht schon als installierte App läuft (Safari/iOS feuert
"beforeinstallprompt" nie — dort bleibt "Zum Home-Bildschirm" über das
Teilen-Menü weiterhin der einzige Weg, das ist eine Apple-Plattform-
Grenze, kein Bug dieser Seite). */
let installEvent = null;
const laeuftSchonAlsApp = () =>
window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
installEvent = e;
const btn = document.getElementById("pwa-install-btn");
if (btn && !laeuftSchonAlsApp()) btn.style.display = "";
});
document.addEventListener("click", (e) => {
if (e.target.closest && e.target.closest("#pwa-install-btn") && installEvent) {
installEvent.prompt();
installEvent.userChoice.finally(() => {
installEvent = null;
const btn = document.getElementById("pwa-install-btn");
if (btn) btn.style.display = "none";
});
}
});
window.addEventListener("appinstalled", () => {
const btn = document.getElementById("pwa-install-btn");
if (btn) btn.style.display = "none";
});
})(); })();
+93
View File
@@ -0,0 +1,93 @@
/* =====================================================================
stimmen.js — NUR für stimmen.html (20.08.2026, Nutzer-Wunsch: "eine
richtig geile kachel ... wo die leute mit namen und tiktok namen mir
eine nachricht schreiben können die ich in die verwaltung kriege").
Formular schickt eine Einreichung ab (landet als Entwurf, siehe
server-internal/routes/testimonials.js), zweiter Teil lädt die bereits
freigegebenen Stimmen und zeigt sie an — bleibt komplett unsichtbar,
solange keine einzige freigegeben wurde. ===================================================================== */
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
const formular = document.getElementById("stimmen-formular");
if (formular) {
const erfolgEl = document.getElementById("stimmen-erfolg");
const fehlerEl = document.getElementById("stimmen-fehler");
formular.addEventListener("submit", async (e) => {
e.preventDefault();
erfolgEl.classList.remove("zeige");
fehlerEl.classList.remove("zeige");
const name = document.getElementById("st-name").value.trim();
const tiktokUsername = document.getElementById("st-tiktok").value.trim();
const nachricht = document.getElementById("st-nachricht").value.trim();
const absendenBtn = formular.querySelector("button[type=submit]");
absendenBtn.disabled = true;
try {
const res = await fetch(`${API_BASE_URL}/testimonials/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, tiktokUsername, nachricht }),
});
const data = await res.json();
if (!data.ok) {
fehlerEl.textContent = data.error || "Einreichen hat leider nicht geklappt. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
return;
}
formular.hidden = true;
erfolgEl.classList.add("zeige");
} catch {
fehlerEl.textContent = "Verbindung zum Server fehlgeschlagen. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
} finally {
absendenBtn.disabled = false;
}
});
}
function escapeHtml(text) {
return String(text ?? "").replace(/[&<>"']/g, (z) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[z]));
}
/* Erstes sichtbares Zeichen des Namens fürs Avatar-Medaillon. [...name]
statt name[0], damit auch Namen, die mit einem Emoji beginnen, nicht
mitten in einem Zeichen abgeschnitten werden (ein Emoji besteht in
JavaScript aus mehreren "Zeichen" — name[0] lieferte sonst nur ein
kaputtes Halb-Zeichen). */
function erstesZeichen(name) {
const zeichen = [...String(name || "").trim()];
return zeichen.length ? zeichen[0].toUpperCase() : "?";
}
function ladeEchteStimmen() {
fetch(`${API_BASE_URL}/testimonials`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
const liste = data && data.ok ? data.testimonials : [];
if (!liste.length) return; // Abschnitt bleibt hidden, kein Platzhalter mehr
const root = document.getElementById("stimmen-echte-root");
const grid = document.getElementById("stimmen-echte-grid");
grid.innerHTML = liste
.map(
(t) => `<div class="stimmen-karte">
<p class="stimmen-karte-text">${escapeHtml(t.nachricht)}</p>
<div class="stimmen-karte-fuss">
<span class="stimmen-karte-avatar" aria-hidden="true">${escapeHtml(erstesZeichen(t.name))}</span>
<span class="stimmen-karte-namen">
<span class="stimmen-karte-name">${escapeHtml(t.name)}</span>
${t.tiktokUsername ? `<span class="stimmen-karte-tiktok">@${escapeHtml(t.tiktokUsername)}</span>` : ""}
</span>
</div>
</div>`
)
.join("");
root.hidden = false;
})
.catch(() => {
/* Abschnitt bleibt einfach unsichtbar -- kein Fehler nötig. */
});
}
ladeEchteStimmen();
})();
+230
View File
@@ -0,0 +1,230 @@
/* =====================================================================
streamplan.js — "Live-Radar" NUR für streamplan.html (20.08.2026,
Nutzer-Wunsch: "soll viel geiler und spezieller sein, überrasch mich",
ergänzt um eine zweite Uhr für ein "spezielles Live-Event": "sehr
wichtig ist dass ich auch spezielle live events da auch eintragen kann
in der verwaltungsseite so dass da eine zweite uhr erscheint wo die
zeit dann für dieses spezielle event läuft").
Zwei 24-Stunden-Ziffernblätter (reines SVG, keine Bild-Assets nötig):
- Uhr 1 (immer sichtbar): der feste 20-Uhr-Termin leuchtet dauerhaft als
Fixpunkt, ein "Jetzt"-Zeiger bewegt sich mit der echten Uhrzeit der
besuchenden Person mit. Live laut /live-status -> Rot/Puls +
"Jetzt anschauen".
- Uhr 2 (nur wenn in der Verwaltung aktiviert): zählt zu einem exakten
Zieldatum+Uhrzeit runter (Tage möglich, nicht nur Stunden), Magenta/
Violett statt Cyan/Gold, damit beide Uhren auf einen Blick
unterscheidbar sind ("bissl Farbe"). ===================================================================== */
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
const SVG_NS = "http://www.w3.org/2000/svg";
const CX = 150, CY = 150, R_OUT = 130, R_TICK_IN = 116, R_TICK_MAJOR_IN = 106, R_HAND = 100;
function polar(r, stundenAnteil) {
const winkel = (stundenAnteil / 24) * 360 - 90; // 0 Uhr oben, im Uhrzeigersinn
const rad = (winkel * Math.PI) / 180;
return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) };
}
/** Zeichnet die 24 Stundenstriche + einen Fixpunkt-Leuchtpunkt bei
"fixStunde" (kann eine Dezimalzahl sein, z.B. 19.5 = 19:30) in die
übergebenen <g>-Container. Läuft einmalig pro Uhr. */
function zeichneZiffernblatt(ticksHost, fixHost, fixStunde) {
ticksHost.innerHTML = "";
for (let h = 0; h < 24; h++) {
const major = h % 6 === 0;
const p1 = polar(R_OUT, h);
const p2 = polar(major ? R_TICK_MAJOR_IN : R_TICK_IN, h);
const line = document.createElementNS(SVG_NS, "line");
line.setAttribute("x1", p1.x); line.setAttribute("y1", p1.y);
line.setAttribute("x2", p2.x); line.setAttribute("y2", p2.y);
line.setAttribute("class", major ? "radar-tick radar-tick-major" : "radar-tick");
ticksHost.appendChild(line);
}
fixHost.innerHTML = "";
if (fixStunde != null) {
const fixPos = polar(R_OUT + 11, fixStunde);
const fixDot = document.createElementNS(SVG_NS, "circle");
fixDot.setAttribute("cx", fixPos.x); fixDot.setAttribute("cy", fixPos.y);
fixDot.setAttribute("r", 6.5);
fixDot.setAttribute("class", "radar-fixmarker-dot");
fixHost.appendChild(fixDot);
}
}
function zeigerAktualisieren(handEl, dotEl, stundenAnteil) {
const spitze = polar(R_HAND, stundenAnteil);
handEl.setAttribute("x2", spitze.x); handEl.setAttribute("y2", spitze.y);
dotEl.setAttribute("cx", spitze.x); dotEl.setAttribute("cy", spitze.y);
}
function zweistellig(n) { return String(n).padStart(2, "0"); }
/* ===================================================================
Uhr 1: täglicher 20-Uhr-Fixtermin (bereits bestehend, unverändert im
Verhalten) ================================================================= */
const radar = document.getElementById("stream-radar");
if (radar) {
const FIX_STUNDE = 20;
zeichneZiffernblatt(document.getElementById("radar-ticks"), document.getElementById("radar-fixmarker"), FIX_STUNDE);
const nowHand = document.getElementById("radar-now-hand");
const nowDot = document.getElementById("radar-now-dot");
function zeigerTick() {
const jetzt = new Date();
zeigerAktualisieren(nowHand, nowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
}
zeigerTick();
setInterval(zeigerTick, 15000);
function naechsterFixTermin() {
const jetzt = new Date();
const ziel = new Date(jetzt);
ziel.setHours(FIX_STUNDE, 0, 0, 0);
if (ziel <= jetzt) ziel.setDate(ziel.getDate() + 1);
return ziel;
}
const countdownEl = document.getElementById("radar-countdown");
const statusText = document.getElementById("radar-status-text");
const statusSub = document.getElementById("radar-status-sub");
const watchBtn = document.getElementById("radar-watch-btn");
let istLive = false;
let letzterTitel = null;
function countdownTick() {
if (istLive) return;
const diffMs = naechsterFixTermin() - new Date();
const gesamtSek = Math.max(0, Math.floor(diffMs / 1000));
const std = Math.floor(gesamtSek / 3600);
const min = Math.floor((gesamtSek % 3600) / 60);
const sek = gesamtSek % 60;
countdownEl.textContent = `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
setInterval(countdownTick, 1000);
function texteNeuSetzen() {
const t = window.dogiUebersetzen || ((k) => k);
if (istLive) {
statusText.textContent = t("sp_radar_live");
countdownEl.textContent = letzterTitel || t("sp_radar_live_generic");
statusSub.textContent = "";
watchBtn.hidden = false;
} else {
statusText.textContent = t("sp_radar_offline");
statusSub.textContent = t("sp_radar_countdown_label");
watchBtn.hidden = true;
countdownTick();
}
}
function liveStatusAnwenden(data) {
istLive = !!(data && data.live);
letzterTitel = data && data.title ? data.title : null;
radar.classList.toggle("ist-live", istLive);
texteNeuSetzen();
}
function liveStatusLaden() {
return fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" })
.then((res) => res.json())
.then(liveStatusAnwenden)
.catch(() => {
istLive = false;
radar.classList.remove("ist-live");
texteNeuSetzen();
});
}
liveStatusLaden();
setInterval(liveStatusLaden, 30000);
document.addEventListener("visibilitychange", () => { if (!document.hidden) liveStatusLaden(); });
document.addEventListener("dogi-sprache-geaendert", texteNeuSetzen);
}
/* ===================================================================
Uhr 2: "spezielles Live-Event" aus der Verwaltung (20.08.2026, neu).
Nur sichtbar, wenn /special-event ein aktives, noch bevorstehendes
Event liefert. Zieluhrzeit kann Tage entfernt sein -> Countdown zeigt
bei Bedarf "Xd HH:MM:SS" statt nur HH:MM:SS. ================================================================= */
const seWrap = document.getElementById("se-radar-wrap");
if (seWrap) {
const seRadar = document.getElementById("se-radar");
const seNowHand = document.getElementById("se-radar-now-hand");
const seNowDot = document.getElementById("se-radar-now-dot");
const seCountdown = document.getElementById("se-radar-countdown");
const seTitel = document.getElementById("se-radar-titel");
const seLink = document.getElementById("se-radar-link");
let zielDatum = null;
let countdownIntervalId = null;
let zeigerIntervalId = null;
function seCountdownTick() {
if (!zielDatum) return;
const diffMs = zielDatum - new Date();
if (diffMs <= 0) {
seRadar.classList.add("ist-live");
seCountdown.textContent = window.dogiUebersetzen ? window.dogiUebersetzen("sp_radar_live_generic") : "Live!";
clearInterval(countdownIntervalId);
return;
}
const gesamtSek = Math.floor(diffMs / 1000);
const tage = Math.floor(gesamtSek / 86400);
const std = Math.floor((gesamtSek % 86400) / 3600);
const min = Math.floor((gesamtSek % 3600) / 60);
const sek = gesamtSek % 60;
seCountdown.textContent = tage > 0
? `${tage}T ${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`
: `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
// Der "Jetzt"-Zeiger zeigt wie bei Uhr 1 die ECHTE aktuelle Uhrzeit --
// der magentafarbene Fixpunkt markiert dagegen die Tageszeit, zu der
// das spezielle Event stattfindet. So bleibt die Bildsprache zwischen
// beiden Uhren konsistent (nur der Fixpunkt unterscheidet sich).
function seZeigerTick() {
const jetzt = new Date();
zeigerAktualisieren(seNowHand, seNowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
}
function specialEventLaden() {
return fetch(`${API_BASE_URL}/special-event`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
const event = data && data.ok ? data.event : null;
if (countdownIntervalId) clearInterval(countdownIntervalId);
if (zeigerIntervalId) clearInterval(zeigerIntervalId);
if (!event) {
seWrap.hidden = true;
zielDatum = null;
return;
}
zielDatum = new Date(event.zieldatum);
seWrap.hidden = false;
seRadar.classList.remove("ist-live");
seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || "";
zeichneZiffernblatt(document.getElementById("se-radar-ticks"), document.getElementById("se-radar-fixmarker"), zielDatum.getHours() + zielDatum.getMinutes() / 60);
seZeigerTick();
seCountdownTick();
zeigerIntervalId = setInterval(seZeigerTick, 15000);
countdownIntervalId = setInterval(seCountdownTick, 1000);
if (event.link) {
seLink.href = event.link;
seLink.hidden = false;
} else {
seLink.hidden = true;
}
})
.catch(() => {
seWrap.hidden = true;
});
}
specialEventLaden();
setInterval(specialEventLaden, 60000); // reicht -- Aenderungen in der Verwaltung muessen nicht sekundengenau ankommen
document.addEventListener("dogi-sprache-geaendert", () => {
if (zielDatum) specialEventLaden();
});
}
})();
+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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/forms.js?v=20260821n"></script>
<script src="assets/js/i18n-bewerben-creator.js"></script> <script src="assets/js/i18n-bewerben-creator.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+11 -8
View File
@@ -1,6 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" data-theme="spicymedia"> <html lang="de" data-theme="dogfather">
<head> <head>
<!-- Nutzer-Wunsch 20.08.2026: neues Hero-Bild (Hasi Dog/DogFather/Husky, babyblau statt
rot) + Theme von "spicymedia" (Rot/Orange) auf "dogfather" (Babyblau/Lila) umgestellt,
damit Knopf-/Link-/Eyebrow-Farben zum neuen, blauen Bild passen. -->
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kooperation anfragen — DOGFATHER UNIVERSE</title> <title>Kooperation anfragen — DOGFATHER UNIVERSE</title>
@@ -19,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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -65,8 +68,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821n"></script>
<script src="assets/js/i18n-bewerben-kooperation.js"></script> <script src="assets/js/i18n-bewerben-kooperation.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -19,10 +19,10 @@
<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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -79,8 +79,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821n"></script>
<script src="assets/js/i18n-bewerben-modi.js"></script> <script src="assets/js/i18n-bewerben-modi.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -19,10 +19,10 @@
<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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -71,8 +71,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821n"></script>
<script src="assets/js/i18n-bewerben-scout.js"></script> <script src="assets/js/i18n-bewerben-scout.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+7 -7
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-bewerben-split.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-bewerben-split.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="bw_eyebrow">Werde Teil vom Team</span> <span class="eyebrow" data-i18n="bw_eyebrow">Werde Teil vom Team</span>
<h1 data-i18n="bw_h1">Community oder Agentur — such dir deinen Weg</h1> <h1 data-i18n="bw_h1">Community oder Agentur — such dir deinen&nbsp;Weg</h1>
<p class="lead worlds" data-i18n="bw_lead">Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.</p> <p class="lead worlds" data-i18n="bw_lead">Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.</p>
</div> </div>
</section> </section>
@@ -85,7 +85,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-bewerben.js"></script> <script src="assets/js/i18n-bewerben.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/i18n-casper.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
@@ -0,0 +1,32 @@
-- =====================================================================
-- Supporter-Abstimmungen (20.08.2026) — "nächste Ausbaustufe" aus
-- Dogfather_VanVan_Supporter_Abo.odt Abschnitt 20 (Bonus-Features, dort
-- bewusst nicht in den verbindlichen Abnahmekriterien Abschnitt 34, siehe
-- Supporter-Abo-System.md "Bewusst nicht Teil dieser ersten Ausbaustufe").
-- Dogi erstellt in der Verwaltung eine Frage + Antwortoptionen auf Deutsch,
-- die anderen Sprachen werden wie bei "Event des Jahres" automatisch beim
-- Speichern übersetzt (lib/translate.js). Jeder DogiCrew-Supporter darf pro
-- Abstimmung genau einmal abstimmen (UNIQUE-Constraint statt Anwendungslogik
-- — robust auch bei gleichzeitigen Anfragen).
-- =====================================================================
CREATE TABLE IF NOT EXISTS supporter_polls (
id TEXT PRIMARY KEY,
question TEXT NOT NULL, -- JSON {de, "de-CH", en, fr, pt}
options TEXT NOT NULL, -- JSON-Array von {de, "de-CH", en, fr, pt}
status TEXT NOT NULL DEFAULT 'active', -- active | closed
created_at TEXT NOT NULL,
created_by TEXT NOT NULL,
closed_at TEXT
);
CREATE TABLE IF NOT EXISTS supporter_poll_votes (
id TEXT PRIMARY KEY,
poll_id TEXT NOT NULL REFERENCES supporter_polls(id),
supporter_id TEXT NOT NULL REFERENCES supporter_users(id),
option_index INTEGER NOT NULL,
voted_at TEXT NOT NULL,
UNIQUE(poll_id, supporter_id)
);
CREATE INDEX IF NOT EXISTS idx_supporter_poll_votes_poll ON supporter_poll_votes (poll_id);
@@ -0,0 +1,23 @@
-- =====================================================================
-- "Stimmen" / Testimonials (20.08.2026) — Fans können auf stimmen.html
-- selbst eine Nachricht mit Name + TikTok-Namen einreichen. Landet erst
-- als Entwurf in der Verwaltung ("eingereicht") -- öffentlich sichtbar
-- wird ein Eintrag NUR, wenn Dogi/VanVan ihn dort ausdrücklich freigeben
-- (Nutzer-Wunsch: "die ausgewählten über die verwaltungsseite auf die
-- website hinzufügen"). Eigene Tabelle statt app_settings, weil dies eine
-- WACHSENDE Liste ist (beliebig viele Einreichungen), keine feste Anzahl
-- Slots wie "Event des Jahres".
-- =====================================================================
CREATE TABLE IF NOT EXISTS testimonials (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
tiktok_username TEXT,
nachricht TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'eingereicht', -- eingereicht | freigegeben | abgelehnt
created_at TEXT NOT NULL,
entschieden_am TEXT,
entschieden_von TEXT
);
CREATE INDEX IF NOT EXISTS idx_testimonials_status ON testimonials (status, created_at);
@@ -0,0 +1,44 @@
-- =====================================================================
-- Team-Mitglieder (21.08.2026) — Nutzer-Wunsch: "überall wo Modis sind
-- oder Scouts oder Manager, oder Creator, will ich dass ich die easy über
-- meine Verwaltungsseite hinzufüge und die automatisch in der Website
-- hinzugefügt werden ... auch mit Fotos und TikTok Link."
--
-- Eine gemeinsame Tabelle für alle vier Kategorien (statt vier eigenen),
-- weil sie strukturell identisch sind (Name/Rolle/Foto/TikTok/Bio/Zitat) --
-- "kategorie" unterscheidet, wo jemand öffentlich erscheint. rang/
-- rang_label sind nur für Modis relevant (Pyramiden-Hierarchie auf
-- team-modis.html), bei den anderen Kategorien bleiben sie einfach leer.
--
-- Ergänzt die weiterhin bestehenden, von Hand gepflegten Einträge in
-- assets/js/data-modis.js/data-scouts.js/data-creator.js/data-manager.js
-- (z.B. VanVan, Patrick, Bananenstift) -- die Website mischt beide Quellen
-- zu einer gemeinsamen Liste (siehe window.dogiTeamLaden() in main.js).
-- Ein Fehler bei einer Kategorie kann so nie die schon bestehenden,
-- fest einprogrammierten Profile verschwinden lassen.
--
-- slug ist GLOBAL eindeutig (nicht nur pro Kategorie), weil profil.html
-- über alle Kategorien hinweg nach genau einem slug sucht.
-- =====================================================================
CREATE TABLE IF NOT EXISTS team_members (
id TEXT PRIMARY KEY,
kategorie TEXT NOT NULL, -- modi | scout | creator | manager
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
rolle_json TEXT NOT NULL, -- {de, "de-CH", en, fr, pt}
foto_url TEXT,
tiktok_url TEXT,
bio_json TEXT, -- {de, "de-CH", en, fr, pt} oder NULL
zitat_json TEXT, -- {de, "de-CH", en, fr, pt} oder NULL
rang INTEGER, -- nur Modi-Pyramide, sonst NULL
rang_label_json TEXT, -- nur Modi-Pyramide, sonst NULL
reihenfolge INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'aktiv', -- aktiv | pausiert
erstellt_am TEXT NOT NULL,
geaendert_am TEXT NOT NULL,
erstellt_von TEXT,
geaendert_von TEXT
);
CREATE INDEX IF NOT EXISTS idx_team_members_kategorie ON team_members (kategorie, status, reihenfolge);
@@ -0,0 +1,18 @@
-- =====================================================================
-- Team-Mitglieder: volle Feld-Parität mit den von Hand gepflegten
-- Profilen (21.08.2026, Nutzer-Wunsch: "wenn ich eine änderung mache
-- ist es auch für jeden für die die schon da sind und die neuen").
--
-- Damit ein bestehendes Profil (Patrick, Bananenstift, Diene, ...) 1:1
-- in die Verwaltung importiert werden kann, OHNE Inhalt zu verlieren,
-- braucht team_members dieselben optionalen Felder wie
-- data-modis.js/data-scouts.js: intro (kurzer Text neben dem Foto),
-- bio_html (ausführliche, mehrabsatzige Vorstellung mit Überschriften),
-- extra_cta (zweiter Button auf der Profilkarte, aktuell nur bei Marina
-- -> manager.html genutzt).
-- =====================================================================
ALTER TABLE team_members ADD COLUMN intro_json TEXT;
ALTER TABLE team_members ADD COLUMN bio_html_json TEXT;
ALTER TABLE team_members ADD COLUMN extra_cta_href TEXT;
ALTER TABLE team_members ADD COLUMN extra_cta_label_json TEXT;
+8 -8
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -45,8 +45,8 @@
<p data-i18n="com_mehr_2">Discord ist das Community-Zentrum — hier trifft man sich, tauscht sich aus und ist Teil des Alltags von Team Dogi.</p> <p data-i18n="com_mehr_2">Discord ist das Community-Zentrum — hier trifft man sich, tauscht sich aus und ist Teil des Alltags von Team Dogi.</p>
<a class="btn btn-primary" href="links.html" data-i18n="com_discord_btn">Discord beitreten</a> <a class="btn btn-primary" href="links.html" data-i18n="com_discord_btn">Discord beitreten</a>
</div> </div>
<div class="img-placeholder" style="aspect-ratio:4/3;"> <div class="collage-photo" style="aspect-ratio:3/4;border-radius:var(--radius);">
<span data-i18n="com_bild_platzhalter">Community-Bild</span><br><span class="small" data-i18n="com_foto_folgt">Foto folgt</span> <img src="assets/img/community-team-dogi.jpg" alt="Diene, Ghost, VanVan und Dogi bei einem gemeinsamen Treffen von Team Dogi" style="width:100%;height:100%;object-fit:cover;object-position:center 25%;" />
</div> </div>
</div> </div>
</section> </section>
@@ -84,7 +84,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-community.js"></script> <script src="assets/js/i18n-community.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+31 -10
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-creator.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-creator.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="crt_eyebrow">Manager DogFather — Team</span> <span class="eyebrow" data-i18n="crt_eyebrow">Manager DogFather — Team</span>
<h1 data-i18n="crt_h1">Creator im Team</h1> <h1 data-i18n="crt_h1" data-site-text-key="seitentitel_creator">Creator im Team</h1>
<p class="lead worlds" data-i18n="crt_lead">Content Creator, die bei DogFather / Spicy Media mitmachen.</p> <p class="lead worlds" data-i18n="crt_lead">Content Creator, die bei DogFather / Spicy Media mitmachen.</p>
</div> </div>
</section> </section>
@@ -49,6 +49,7 @@
<span data-i18n="crt_empty">Noch keine Creator-Profile veröffentlicht — bald verfügbar. 🩵</span> <span data-i18n="crt_empty">Noch keine Creator-Profile veröffentlicht — bald verfügbar. 🩵</span>
<span data-i18n="crt_filter_alle">alle</span> <span data-i18n="crt_filter_alle">alle</span>
<span data-i18n="crt_profil_btn">Profil ansehen</span> <span data-i18n="crt_profil_btn">Profil ansehen</span>
<span data-i18n="crt_kategorie_sonstige">Sonstige</span>
</div> </div>
<!-- TODO: Noch keine Creator-Profile final geklärt. Sobald Namen/Infos feststehen, in assets/js/data-creator.js ergänzen — diese Seite rendert automatisch. --> <!-- TODO: Noch keine Creator-Profile final geklärt. Sobald Namen/Infos feststehen, in assets/js/data-creator.js ergänzen — diese Seite rendert automatisch. -->
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
@@ -59,14 +60,29 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-creator.js"></script> <script src="assets/js/data-creator.js?v=20260821n"></script>
<script src="assets/js/i18n-creator.js"></script> <script src="assets/js/i18n-creator.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
const grid = document.getElementById("creator-grid"); const grid = document.getElementById("creator-grid");
const filterBar = document.getElementById("creator-filters"); const filterBar = document.getElementById("creator-filters");
const list = window.CREATORS || []; let list = window.CREATORS || [];
// 21.08.2026: über die Verwaltung angelegte Creator ergänzen, BEVOR
// entschieden wird, ob die Seite "leer" ist (Nutzer-Wunsch "überall wo
// ... Creator ... sind, will ich dass ich die easy über meine
// Verwaltungsseite hinzufüge"). Vorher gab es direkt darunter ein
// "return", sobald window.CREATORS leer war (bisher der Normalfall, da
// noch keine echten Creator-Profile feststehen) -- dadurch wären
// admin-gepflegte Creator NIE geladen worden.
if (window.dogiTeamLaden) {
const neue = await window.dogiTeamLaden("creator");
// Runde 2 (21.08.2026, "wenn ich eine änderung mache ist es auch für
// jeden"): überschreibt bereits importierte Creator (gleicher slug)
// statt sie zu duplizieren.
list = window.dogiTeamZusammenfuehren(list, neue);
}
// Texte fürs JS-generierte Grid/Filter kommen aus den versteckten // Texte fürs JS-generierte Grid/Filter kommen aus den versteckten
// data-i18n-Elementen oben — main.js übersetzt sie ganz normal (auch bei // data-i18n-Elementen oben — main.js übersetzt sie ganz normal (auch bei
@@ -88,6 +104,11 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
// 21.08.2026: über die Verwaltung angelegte Creator haben (noch) kein
// eigenes Genre-Feld (category) -- ohne diesen Fallback würde die
// Filterleiste einen kaputten "undefined"-Knopf zeigen.
list = list.map((c) => (c.category ? c : { ...c, category: uebersetzen("crt_kategorie_sonstige") }));
const categories = ["alle", ...new Set(list.map((c) => c.category))]; const categories = ["alle", ...new Set(list.map((c) => c.category))];
let aktiverFilter = "alle"; let aktiverFilter = "alle";
+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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/i18n-dogfather.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<style> <style>
/* ===================================================================== /* =====================================================================
Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern
+10 -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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -56,6 +56,10 @@
<div class="container grid grid-2" style="align-items:center;"> <div class="container grid grid-2" style="align-items:center;">
<div> <div>
<h2 data-i18n="hd_welt_h2">🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie</h2> <h2 data-i18n="hd_welt_h2">🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie</h2>
<!-- Eigener TikTok-Account von HasiDog (Nutzer-Wunsch 20.08.2026) -->
<p style="margin:.2rem 0 1.2rem;">
<a class="btn-tiktok" href="https://www.tiktok.com/@hasidog0804" target="_blank" rel="noopener"><span class="btn-tiktok-note" aria-hidden="true">♪</span><span class="btn-tiktok-text" data-i18n="hd_btn_tiktok">Zur HasiDog TikTok-Seite</span></a>
</p>
<p data-i18n="hd_welt_1">HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵</p> <p data-i18n="hd_welt_1">HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵</p>
<p data-i18n="hd_welt_2">Hier treffen freche Ideen auf große Gefühle, verrückte Momente auf echte Freundschaft und Fantasie auf Abenteuer. HasiDog steht vollkommen für sich – mit einem eigenen Charakter, einer eigenen Geschichte und einer eigenen Familie.</p> <p data-i18n="hd_welt_2">Hier treffen freche Ideen auf große Gefühle, verrückte Momente auf echte Freundschaft und Fantasie auf Abenteuer. HasiDog steht vollkommen für sich – mit einem eigenen Charakter, einer eigenen Geschichte und einer eigenen Familie.</p>
<p class="quote" data-i18n="hd_welt_quote" style="margin-top:1.4rem;">Frech im Kopf. Chaotisch im Herzen. Einzigartig in jeder Geschichte.</p> <p class="quote" data-i18n="hd_welt_quote" style="margin-top:1.4rem;">Frech im Kopf. Chaotisch im Herzen. Einzigartig in jeder Geschichte.</p>
@@ -182,7 +186,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-hasidog.js"></script> <script src="assets/js/i18n-hasidog.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+181 -54
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</head> </head>
<body class="start-seite"> <body class="start-seite">
<div id="site-header"></div> <div id="site-header"></div>
@@ -81,9 +81,57 @@
<p data-i18n="idx_hero_card3_desc">Als Manager bei Spicy Media: Scouts, Creator und faire Zusammenarbeit auf Augenhöhe.</p> <p data-i18n="idx_hero_card3_desc">Als Manager bei Spicy Media: Scouts, Creator und faire Zusammenarbeit auf Augenhöhe.</p>
</a> </a>
</div> </div>
<!-- "Event des Jahres": volle Breite wie die 3 Welt-Kacheln zusammen. Seit
20.08.2026 (Nutzer-Wunsch) NUR sichtbar, wenn Dogi/VanVan mindestens ein
Event in der Verwaltung ausgefüllt UND aktiviert haben -- kein
Standard-/Platzhalterinhalt mehr, kompletter Abschnitt bleibt bis dahin
unsichtbar (siehe zeigeJahresEvents() weiter unten). Klick öffnet ein
großes Detail-Fenster (Bild/Titel/Text größer, optionaler direkter Link)
statt sofort wegzunavigieren. -->
<div class="jahres-events" id="jahres-events" hidden>
<div class="jahres-events-head">
<span class="tag" data-i18n="idx_jahres_events_tag">🏆 Event des Jahres</span>
</div>
<div class="jahres-events-grid" id="jahres-events-grid">
<button type="button" class="jahres-event-card" id="jahres-event-1" data-slot="1" hidden>
<span class="jahres-event-shine" aria-hidden="true"></span>
<img class="jahres-event-bild" alt="" hidden />
<span class="jahres-event-datum"></span>
<h3></h3>
<p></p>
<span class="jahres-event-mehr" data-i18n="idx_jahres_events_mehr">Mehr erfahren →</span>
</button>
<button type="button" class="jahres-event-card" id="jahres-event-2" data-slot="2" hidden>
<span class="jahres-event-shine" aria-hidden="true"></span>
<img class="jahres-event-bild" alt="" hidden />
<span class="jahres-event-datum"></span>
<h3></h3>
<p></p>
<span class="jahres-event-mehr" data-i18n="idx_jahres_events_mehr">Mehr erfahren →</span>
</button>
</div>
</div>
</div> </div>
</section> </section>
<!-- Detail-Fenster für "Event des Jahres" -- bewusst außerhalb der Hero-Sektion
(die hat eigene transform-/overflow-Effekte für die Polarlicht-/Nebel-
Animation, die ein position:fixed-Fenster aus der Zentrierung bringen
könnten). -->
<div class="jahres-event-modal-overlay" id="jahres-event-modal-overlay" hidden>
<div class="jahres-event-modal" role="dialog" aria-modal="true" aria-labelledby="jahres-event-modal-titel">
<button type="button" class="jahres-event-modal-close" id="jahres-event-modal-close" aria-label="Schließen">✕</button>
<img class="jahres-event-modal-bild" id="jahres-event-modal-bild" alt="" hidden />
<div class="jahres-event-modal-body">
<span class="jahres-event-modal-datum" id="jahres-event-modal-datum"></span>
<h2 id="jahres-event-modal-titel"></h2>
<p id="jahres-event-modal-text"></p>
<a class="btn btn-primary jahres-event-modal-link" id="jahres-event-modal-link" href="#" target="_blank" rel="noopener" hidden></a>
</div>
</div>
</div>
<!-- ================= CHARAKTERE: DOGFATHER-HUSKY & HASIDOG-HASE ================= --> <!-- ================= CHARAKTERE: DOGFATHER-HUSKY & HASIDOG-HASE ================= -->
<section class="section-tight"> <section class="section-tight">
<div class="container text-center"> <div class="container text-center">
@@ -180,23 +228,20 @@
<div class="container"> <div class="container">
<h2 class="text-center" data-i18n="idx_team_h2">Team Dogi 🩵</h2> <h2 class="text-center" data-i18n="idx_team_h2">Team Dogi 🩵</h2>
<p class="lead text-center" data-i18n="idx_team_lead" style="max-width:60ch;margin:0 auto;">VanVan & Modis — die Menschen, die DOGFATHER UNIVERSE tragen.</p> <p class="lead text-center" data-i18n="idx_team_lead" style="max-width:60ch;margin:0 auto;">VanVan & Modis — die Menschen, die DOGFATHER UNIVERSE tragen.</p>
<div id="team-preview" class="grid grid-4"></div> <!-- Nutzer-Wunsch 21.08.2026: die vier Personen-Kacheln (VanVan/Diene/
Funny/Miss) hier auf der Startseite entfernt -- Überschrift, Text
und der Knopf zum Modi-Team bleiben. Die Kacheln selbst gibt es
weiterhin vollständig auf team-modis.html. -->
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="team-modis.html" data-i18n="idx_team_btn_modi">Modi-Team ansehen</a> <a class="btn btn-outline" href="team-modis.html" data-i18n="idx_team_btn_modi">Modi-Team ansehen</a>
</div> </div>
</div> </div>
</section> </section>
<!-- ================= AKTIONEN TEASER ================= --> <!-- Nutzer-Wunsch 21.08.2026: der "Was gerade läuft"-Aktionen-Teaser
<section> (Überschrift + Button "Alle Aktionen & Projekte", direkt hier davor)
<div class="container"> wurde entfernt. data-aktionen.js/aktion-detail.html bleiben unangetastet,
<h2 class="text-center" data-i18n="idx_aktionen_h2">Was gerade läuft</h2> nur die Startseiten-Vorschau ist weg. -->
<div id="aktionen-teaser" class="grid grid-3"></div>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="medien-dogfather.html#events" data-i18n="idx_aktionen_btn">Alle Aktionen &amp; Projekte</a>
</div>
</div>
</section>
<!-- ================= GALERIE TEASER ================= --> <!-- ================= GALERIE TEASER ================= -->
<section class="section-tight"> <section class="section-tight">
@@ -245,11 +290,10 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script> <script src="assets/js/data-modis.js?v=20260821n"></script>
<script src="assets/js/data-scouts.js"></script> <script src="assets/js/data-scouts.js?v=20260821n"></script>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/i18n-index.js?v=20260821n"></script>
<script src="assets/js/i18n-index.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script src="assets/js/main.js"></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.
@@ -331,46 +375,129 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
/* "Event des Jahres": seit 20.08.2026 in der Verwaltung pflegbar (Bild,
Text, Link, Aktiv-Schalter je Slot, siehe verwaltung.html +
server-internal/routes/events.js). Nutzer-Wunsch 20.08.2026: "die
kisten sollen leer sein solang wie ich nichts rein setze, am besten
sollen die sogar immer nur erscheinen wenn ich was rein setze und sie
aktiviere" — der Server liefert über /events-of-year ohnehin NUR
aktivierte Slots (inaktive/leere kommen als null), hier also einfach:
kein Eintrag -> Kachel bleibt versteckt, kompletter Abschnitt bleibt
versteckt, wenn beide Slots leer sind. Klick öffnet ein großes
Detail-Fenster statt sofort zu navigieren. */
const feldJahresEvent = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
let jahresEventsDaten = [null, null];
function zeigeJahresEventKachel(slot, daten) {
const karte = document.getElementById(`jahres-event-${slot}`);
if (!karte) return;
if (!daten) {
karte.hidden = true;
return;
}
const bild = karte.querySelector(".jahres-event-bild");
if (daten.bild) {
bild.src = daten.bild;
bild.alt = feldJahresEvent(daten.titel);
bild.hidden = false;
} else {
bild.hidden = true;
}
karte.querySelector(".jahres-event-datum").textContent = daten.datum || "";
karte.querySelector("h3").textContent = feldJahresEvent(daten.titel);
karte.querySelector("p").textContent = feldJahresEvent(daten.text);
karte.hidden = false;
}
function aktualisiereJahresEventsSichtbarkeit() {
const aktiveSlots = jahresEventsDaten.filter(Boolean).length;
const abschnitt = document.getElementById("jahres-events");
const grid = document.getElementById("jahres-events-grid");
abschnitt.hidden = aktiveSlots === 0;
// Genau 1 aktives Event: Kachel zentriert und etwas größer statt halb
// leerem Raster (Nutzer-Wunsch: "kacheln sollen bissl spezieller sein").
grid.classList.toggle("jahres-events-grid-einzeln", aktiveSlots === 1);
}
function jahresEventModalOeffnen(slot) {
const daten = jahresEventsDaten[slot - 1];
if (!daten) return;
const bild = document.getElementById("jahres-event-modal-bild");
if (daten.bild) {
bild.src = daten.bild;
bild.alt = feldJahresEvent(daten.titel);
bild.hidden = false;
} else {
bild.hidden = true;
}
document.getElementById("jahres-event-modal-datum").textContent = daten.datum || "";
document.getElementById("jahres-event-modal-titel").textContent = feldJahresEvent(daten.titel);
document.getElementById("jahres-event-modal-text").textContent = feldJahresEvent(daten.text);
const linkEl = document.getElementById("jahres-event-modal-link");
if (daten.link) {
linkEl.href = daten.link;
linkEl.textContent = feldJahresEvent(window.I18N_PAGE?.idx_jahres_event_modal_link) || "Direkt ansehen →";
linkEl.hidden = false;
} else {
linkEl.hidden = true;
}
const overlay = document.getElementById("jahres-event-modal-overlay");
overlay.hidden = false;
requestAnimationFrame(() => overlay.classList.add("offen"));
document.body.classList.add("modal-offen");
}
function jahresEventModalSchliessen() {
const overlay = document.getElementById("jahres-event-modal-overlay");
overlay.classList.remove("offen");
document.body.classList.remove("modal-offen");
setTimeout(() => { overlay.hidden = true; }, 220);
}
document.getElementById("jahres-event-1")?.addEventListener("click", () => jahresEventModalOeffnen(1));
document.getElementById("jahres-event-2")?.addEventListener("click", () => jahresEventModalOeffnen(2));
document.getElementById("jahres-event-modal-close")?.addEventListener("click", jahresEventModalSchliessen);
document.getElementById("jahres-event-modal-overlay")?.addEventListener("click", (e) => {
if (e.target.id === "jahres-event-modal-overlay") jahresEventModalSchliessen();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !document.getElementById("jahres-event-modal-overlay").hidden) jahresEventModalSchliessen();
});
if (API_BASE_URL) {
const ladeJahresEvents = () =>
fetch(`${API_BASE_URL}/events-of-year`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
if (!data?.ok) return;
jahresEventsDaten = data.events;
zeigeJahresEventKachel(1, data.events[0]);
zeigeJahresEventKachel(2, data.events[1]);
aktualisiereJahresEventsSichtbarkeit();
})
.catch(() => {
/* Kein Fehler nötig -- der Abschnitt bleibt einfach unsichtbar. */
});
ladeJahresEvents();
// Bei Sprachwechsel mit den zuletzt geladenen Daten neu rendern (kein
// erneuter Server-Aufruf nötig) — Titel/Text stehen ja bereits in allen
// Sprachen im geladenen Objekt.
document.addEventListener("dogi-sprache-geaendert", () => {
zeigeJahresEventKachel(1, jahresEventsDaten[0]);
zeigeJahresEventKachel(2, jahresEventsDaten[1]);
});
}
// Stats: Team-Größe aus den Datenobjekten berechnen // Stats: Team-Größe aus den Datenobjekten berechnen
const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length; const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length;
const statTeam = document.getElementById("stat-team"); const statTeam = document.getElementById("stat-team");
if (statTeam) statTeam.textContent = teamCount > 0 ? `${teamCount}+` : "–"; if (statTeam) statTeam.textContent = teamCount > 0 ? `${teamCount}+` : "–";
// Team-Vorschau: VanVan zuerst, dann die nächsten nach Rang, plus 1-2 Scouts // Nutzer-Wunsch 21.08.2026: die Team-Vorschau (vier Personen-Kacheln unter
const teamPreview = document.getElementById("team-preview"); // "Team Dogi 🩵") wurde entfernt -- der zugehörige Render-Code stand hier.
if (teamPreview && window.MODIS) { // data-modis.js/data-scouts.js bleiben eingebunden, weil die Statistik-Zeile
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || ""); // weiter oben ("Menschen im Team Dogi") sie weiterhin auszählt.
const people = [...window.MODIS].sort((a, b) => a.rank - b.rank).slice(0, 4);
const renderTeamPreview = () => {
teamPreview.innerHTML = people
.map(
(p) => `
<a class="profile-card" href="${p.page || `profil.html?p=${p.slug}`}">
<div class="avatar">${(p.avatar || p.photo) ? `<img src="${p.avatar || p.photo}" alt="${p.name}" loading="lazy">` : `<span class="avatar-initial">${p.name.charAt(0).toUpperCase()}</span>`}</div>
<h3>${p.name}</h3>
<div class="role">${feld(p.role)}</div>
</a>`
)
.join("");
};
renderTeamPreview();
document.addEventListener("dogi-sprache-geaendert", renderTeamPreview);
}
// Aktionen-Teaser: die ersten 3 Einträge aus data-aktionen.js
const aktionenTeaser = document.getElementById("aktionen-teaser");
if (aktionenTeaser && window.AKTIONEN) {
aktionenTeaser.innerHTML = window.AKTIONEN.slice(0, 3)
.map(
(a) => `
<a class="card" href="aktion-detail.html?id=${a.id}">
<span class="tag">${a.tag} · ${a.status}</span>
<h3>${a.title}</h3>
<p>${a.summary}</p>
</a>`
)
.join("");
}
}); });
</script> </script>
</body> </body>
+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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/i18n-kontakt.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+6 -6
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<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
@@ -140,8 +140,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-links.js"></script> <script src="assets/js/i18n-links.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
(function () { (function () {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
+66 -6
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -96,6 +96,19 @@
</div> </div>
</section> </section>
<!-- "Weitere Manager" (21.08.2026, Nutzer-Wunsch: "überall wo ... Manager
... sind, will ich dass ich die easy über meine Verwaltungsseite
hinzufüge"). Aktuell gibt es nur DogFather selbst als Manager -- diese
Sektion bleibt deshalb komplett unsichtbar (hidden), bis über die
Verwaltung mindestens ein weiterer Manager angelegt wurde. Gleiches
flaches Karten-Raster wie team-scouts.html. -->
<section class="section-tight" id="weitere-manager-root" hidden>
<div class="container">
<h2 class="text-center" data-i18n="mgr_weitere_h2" data-site-text-key="seitentitel_manager">Weitere Manager</h2>
<div id="weitere-manager-grid" class="grid grid-4"></div>
</div>
</section>
<section> <section>
<div class="container"> <div class="container">
<h2 class="text-center" data-i18n="mgr_team_h2">Willst du dazugehören?</h2> <h2 class="text-center" data-i18n="mgr_team_h2">Willst du dazugehören?</h2>
@@ -126,7 +139,54 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-manager.js"></script> <script src="assets/js/data-manager.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/i18n-manager.js?v=20260821n"></script>
<script src="assets/js/main.js?v=20260821n"></script>
<script>
// "Weitere Manager" (21.08.2026) -- siehe Kommentar bei #weitere-manager-root
// oben. Aktuell IMMER window.MANAGERS === [] (siehe data-manager.js), Inhalt
// kommt also ausschließlich über die Verwaltung (dogiTeamLaden). Bleibt
// komplett unsichtbar, solange niemand eingetragen ist.
document.addEventListener("DOMContentLoaded", () => {
const root = document.getElementById("weitere-manager-root");
const grid = document.getElementById("weitere-manager-grid");
if (!root || !grid) return;
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel);
const renderWeitereManager = () => {
const liste = window.MANAGERS || [];
if (!liste.length) {
root.hidden = true;
return;
}
grid.innerHTML = liste.map((m) => {
const quoteText = feld(m.quote);
return `
<a class="profile-card" href="profil.html?p=${m.slug}">
<div class="avatar">${(m.avatar || m.photo) ? `<img src="${m.avatar || m.photo}" alt="${m.name}" loading="lazy">` : `<span class="avatar-initial">${m.name.charAt(0).toUpperCase()}</span>`}</div>
<h3>${m.name}</h3>
<div class="role">${feld(m.role)}</div>
${quoteText ? `<p class="quote">„${quoteText}"</p>` : ""}
<span class="btn btn-outline" style="margin-top:.7rem;pointer-events:none;">${t("mgr_weitere_profil_btn")}</span>
</a>`;
}).join("");
root.hidden = false;
};
document.addEventListener("dogi-sprache-geaendert", renderWeitereManager);
if (window.dogiTeamLaden) {
window.dogiTeamLaden("manager").then((neue) => {
if (!neue.length) return;
// Runde 2 (21.08.2026, "wenn ich eine änderung mache ist es auch für
// jeden"): überschreibt bereits importierte Manager statt zu duplizieren.
window.MANAGERS = window.dogiTeamZusammenfuehren(window.MANAGERS || [], neue);
renderWeitereManager();
});
}
});
</script>
</body> </body>
</html> </html>
+40
View File
@@ -0,0 +1,40 @@
{
"name": "DogiCrew-Verwaltung",
"short_name": "Verwaltung",
"description": "Interner Admin-Bereich: DogiCrew-Supporter, Prämien, Event des Jahres, Abstimmungen. Nur mit Zugangscode.",
"start_url": "/verwaltung.html",
"id": "/verwaltung.html",
"scope": "/verwaltung.html",
"display": "standalone",
"orientation": "any",
"background_color": "#0e0a16",
"theme_color": "#0e0a16",
"lang": "de",
"dir": "ltr",
"icons": [
{
"src": "/assets/img/icon-192-admin.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/icon-512-admin.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/icon-192-maskable-admin.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/icon-512-maskable-admin.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+9 -9
View File
@@ -19,10 +19,10 @@
<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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -59,10 +59,10 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js"></script> <script src="assets/js/data-media.js?v=20260821n"></script>
<script src="assets/js/data-videos.js"></script> <script src="assets/js/data-videos.js?v=20260821n"></script>
<script src="assets/js/medien-welt.js"></script> <script src="assets/js/medien-welt.js?v=20260821n"></script>
<script src="assets/js/i18n-medien-casper.js"></script> <script src="assets/js/i18n-medien-casper.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/data-media.js?v=20260821n"></script>
<script src="assets/js/data-videos.js"></script> <script src="assets/js/data-videos.js?v=20260821n"></script>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/data-aktionen.js?v=20260821n"></script>
<script src="assets/js/medien-welt.js"></script> <script src="assets/js/medien-welt.js?v=20260821n"></script>
<script src="assets/js/i18n-medien-dogfather.js"></script> <script src="assets/js/i18n-medien-dogfather.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -19,10 +19,10 @@
<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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-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"></script> <script src="assets/js/data-media.js?v=20260821n"></script>
<script src="assets/js/data-videos.js"></script> <script src="assets/js/data-videos.js?v=20260821n"></script>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/data-aktionen.js?v=20260821n"></script>
<script src="assets/js/medien-welt.js"></script> <script src="assets/js/medien-welt.js?v=20260821n"></script>
<script src="assets/js/i18n-medien-hasidog.js"></script> <script src="assets/js/i18n-medien-hasidog.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+33 -15
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<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,18 +130,25 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script> <script src="assets/js/data-modis.js?v=20260821n"></script>
<script src="assets/js/data-scouts.js"></script> <script src="assets/js/data-scouts.js?v=20260821n"></script>
<script src="assets/js/data-creator.js"></script> <script src="assets/js/data-creator.js?v=20260821n"></script>
<script src="assets/js/i18n-profil.js"></script> <script src="assets/js/data-manager.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/i18n-profil.js?v=20260821n"></script>
<script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
const slug = new URLSearchParams(location.search).get("p") || ""; const slug = new URLSearchParams(location.search).get("p") || "";
const modis = window.MODIS || []; const modis = window.MODIS || [];
const scouts = window.SCOUTS || []; const scouts = window.SCOUTS || [];
const creators = window.CREATORS || []; const creators = window.CREATORS || [];
const alle = [...modis, ...scouts, ...creators]; const managers = window.MANAGERS || [];
// 21.08.2026: über die Verwaltung angelegte Mitglieder ALLER Kategorien
// dazuholen -- profil.html weiß vorab nicht, zu welcher Kategorie ein
// slug gehört, deshalb kommt hier (ohne Kategorie-Filter) einfach alles
// zusammen zurück und wird unten normal mitgesucht.
const admins = window.dogiTeamLaden ? await window.dogiTeamLaden() : [];
const alle = [...modis, ...scouts, ...creators, ...managers, ...admins];
const person = alle.find((p) => p.slug === slug); const person = alle.find((p) => p.slug === slug);
// Für alle Rahmentexte, die rein per JS gesetzt werden (Name/Rolle/Bio // Für alle Rahmentexte, die rein per JS gesetzt werden (Name/Rolle/Bio
@@ -170,16 +177,22 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
const istScout = scouts.includes(person); // 21.08.2026: admin-gepflegte Personen (aus dogiTeamLaden) stecken NICHT
const istCreator = creators.includes(person); // in den statischen modis/scouts/creators/managers-Arrays selbst (die
const welt = istScout || istCreator ? "spicymedia" : "dogfather"; // reference-Prüfung .includes(person) würde für sie also fälschlich
// "false" liefern) -- deshalb zusätzlich über das von dogiTeamLaden
// gesetzte person.kategorie-Feld prüfen.
const istScout = scouts.includes(person) || person.kategorie === "scout";
const istCreator = creators.includes(person) || person.kategorie === "creator";
const istManager = managers.includes(person) || person.kategorie === "manager";
const welt = istScout || istCreator || istManager ? "spicymedia" : "dogfather";
document.documentElement.setAttribute("data-theme", welt); document.documentElement.setAttribute("data-theme", welt);
// Passendes Weltmotiv nur noch als weichgezeichnete Stimmung ganz im // Passendes Weltmotiv nur noch als weichgezeichnete Stimmung ganz im
// Hintergrund (body.mit-hintergrund) — bewusst KEIN scharfes Banner-Bild // Hintergrund (body.mit-hintergrund) — bewusst KEIN scharfes Banner-Bild
// mehr oben im Kopfbereich, damit das Profil der Person (Foto/Name/Bio) // mehr oben im Kopfbereich, damit das Profil der Person (Foto/Name/Bio)
// sofort beim Öffnen der Seite im Fokus steht, ohne Banner davor. // sofort beim Öffnen der Seite im Fokus steht, ohne Banner davor.
const bgBasis = istScout || istCreator ? "bg-scouts" : "bg-dogfather"; const bgBasis = istScout || istCreator || istManager ? "bg-scouts" : "bg-dogfather";
document.body.style.setProperty("--page-bg", `url('/assets/img/${bgBasis}.jpg')`); document.body.style.setProperty("--page-bg", `url('/assets/img/${bgBasis}.jpg')`);
document.body.style.setProperty("--page-bg-mobile", `url('/assets/img/${bgBasis}-mobile.jpg')`); document.body.style.setProperty("--page-bg-mobile", `url('/assets/img/${bgBasis}-mobile.jpg')`);
@@ -241,6 +254,8 @@ document.addEventListener("DOMContentLoaded", () => {
? t("pr_eyebrow_scout") ? t("pr_eyebrow_scout")
: istCreator : istCreator
? t("pr_eyebrow_creator") ? t("pr_eyebrow_creator")
: istManager
? t("pr_eyebrow_manager")
: t("pr_eyebrow_default"); : t("pr_eyebrow_default");
document.getElementById("profil-hero-name").textContent = person.name; document.getElementById("profil-hero-name").textContent = person.name;
@@ -286,6 +301,9 @@ document.addEventListener("DOMContentLoaded", () => {
} else if (istCreator) { } else if (istCreator) {
zurueck.href = "creator.html"; zurueck.href = "creator.html";
zurueck.textContent = t("pr_back_creator"); zurueck.textContent = t("pr_back_creator");
} else if (istManager) {
zurueck.href = "manager.html";
zurueck.textContent = t("pr_back_manager");
} else { } else {
zurueck.href = "team-modis.html"; zurueck.href = "team-modis.html";
zurueck.textContent = t("pr_back_modis"); zurueck.textContent = t("pr_back_modis");
+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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<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"></script> <script src="assets/js/i18n-rabattcodes.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
(function () { (function () {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
+9 -4
View File
@@ -1,10 +1,11 @@
/* ===================================================================== /* =====================================================================
db.js — SQLite-Datenbank (better-sqlite3), Ersatz für Cloudflare D1. db.js — SQLite-Datenbank (better-sqlite3), Ersatz für Cloudflare D1.
Führt beim ersten Start alle vier Migrationen aus ../cloudflare-worker/migrations/ Führt beim ersten Start alle Migrationen aus ../cloudflare-worker/migrations/
in der ursprünglichen Reihenfolge aus (0001 Basis-Schema, 0002 app_settings, in der ursprünglichen Reihenfolge aus (0001 Basis-Schema, 0002 app_settings,
0003 Supporter-Abo, 0004 TikTok/Discord-Spalten) — anders als bei VanVans Shop 0003 Supporter-Abo, 0004 TikTok/Discord-Spalten, 0005 Supporter-Abstimmungen)
ist hier NIE alles in einer konsolidierten schema.sql zusammengefasst worden, — anders als bei VanVans Shop ist hier NIE alles in einer konsolidierten
deshalb müssen wir hier tatsächlich alle vier der Reihe nach anwenden. schema.sql zusammengefasst worden, deshalb müssen wir hier tatsächlich alle
der Reihe nach anwenden.
===================================================================== */ ===================================================================== */
import Database from "better-sqlite3"; import Database from "better-sqlite3";
@@ -27,6 +28,10 @@ const MIGRATION_FILES = [
"0002_app_settings.sql", "0002_app_settings.sql",
"0003_supporter_abo.sql", "0003_supporter_abo.sql",
"0004_supporter_oauth_providers.sql", "0004_supporter_oauth_providers.sql",
"0005_supporter_polls.sql",
"0006_testimonials.sql",
"0007_team_members.sql",
"0008_team_members_rich.sql",
]; ];
function alreadyApplied(name) { function alreadyApplied(name) {
+93
View File
@@ -27,6 +27,9 @@ import "dotenv/config";
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import cron from "node-cron"; import cron from "node-cron";
import multer from "multer";
import { mkdirSync } from "node:fs";
import { extname } from "node:path";
import { initDb } from "./db.js"; import { initDb } from "./db.js";
import { json } from "./lib/http.js"; import { json } from "./lib/http.js";
@@ -60,6 +63,18 @@ import {
import { import {
listSupporters, getSupporterDetail, updatePremium, listCollections, upsertCollection, getAdminOverview, listSupporters, getSupporterDetail, updatePremium, listCollections, upsertCollection, getAdminOverview,
} from "./routes/supporter-admin.js"; } from "./routes/supporter-admin.js";
import { getEventsPublic, getEventsAdmin, saveEvent, uploadEventImage } from "./routes/events.js";
import { getSpecialEventPublic, getSpecialEventAdmin, saveSpecialEvent } from "./routes/special-event.js";
import {
submitTestimonial, getApprovedTestimonialsPublic, getTestimonialsAdmin, setTestimonialStatus, deleteTestimonial,
} from "./routes/testimonials.js";
import {
createPoll, listPolls, closePoll, reopenPoll, deletePoll, getActivePollPublic, voteOnPoll,
} from "./routes/polls.js";
import {
getTeamPublic, getTeamAdmin, saveTeamMember, importLegacyMember, deleteTeamMember, uploadTeamPhoto,
} from "./routes/team.js";
import { getSiteTextsPublic, saveSiteText } from "./routes/site-texts.js";
initDb(); initDb();
@@ -68,6 +83,39 @@ const app = express();
app.set("trust proxy", 1); app.set("trust proxy", 1);
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack
/* Bild-Uploads für admin-gepflegte Inhalte (aktuell: "Event des Jahres",
20.08.2026). Liegt bewusst NEBEN der Datenbank unter /var/lib/... statt im
Git-Repo-Ordner — Uploads sollen einen `git pull`/Deploy unbeschadet
überstehen und nicht versehentlich mit eingecheckt werden. */
const UPLOADS_BASIS_DIR = process.env.UPLOADS_DIR || "/var/lib/dogfather-internal/uploads";
const EVENTS_UPLOADS_DIR = `${UPLOADS_BASIS_DIR}/events`;
mkdirSync(EVENTS_UPLOADS_DIR, { recursive: true });
// Team-Fotos (21.08.2026, "Verwaltungsseite perfektionieren"-Auftrag) --
// eigener Unterordner, gleiches Muster wie bei den Event-Bildern oben.
const TEAM_UPLOADS_DIR = `${UPLOADS_BASIS_DIR}/team`;
mkdirSync(TEAM_UPLOADS_DIR, { recursive: true });
const ERLAUBTE_BILD_TYPEN = new Set(["image/jpeg", "image/png", "image/webp"]);
const bildUpload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, EVENTS_UPLOADS_DIR),
// Zufälliger Dateiname (crypto.randomUUID) statt des Originalnamens —
// verhindert Pfad-/Namenskollisionen und Path-Traversal-Versuche über
// einen manipulierten Dateinamen komplett, ganz ohne Validierung nötig.
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)),
});
const teamBildUpload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, TEAM_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");
@@ -76,6 +124,10 @@ app.use((req, res, next) => {
}); });
app.use(cors()); app.use(cors());
// Bild-Uploads oeffentlich ausliefern (index.html zeigt "Event des Jahres"-Bilder
// JEDER Besucherin/JEDEM Besucher, ohne Login) — nur lesend, Schreiben bleibt
// ausschliesslich ueber /admin/events/upload-image mit EVENTS_MANAGE-Berechtigung.
app.use("/uploads", express.static(UPLOADS_BASIS_DIR));
app.use( app.use(
express.json({ express.json({
limit: "2mb", limit: "2mb",
@@ -124,6 +176,12 @@ app.get("/health", (req, res) => json(res, { ok: true, service: "dogfather-unive
app.post("/submit", wrap(submitApplication)); app.post("/submit", wrap(submitApplication));
app.get("/live-status", wrap(handleLiveStatusGet)); app.get("/live-status", wrap(handleLiveStatusGet));
app.post("/auth/login", wrap(login)); app.post("/auth/login", wrap(login));
app.get("/events-of-year", wrap(getEventsPublic));
app.get("/special-event", wrap(getSpecialEventPublic));
app.post("/testimonials/submit", wrap(submitTestimonial));
app.get("/testimonials", wrap(getApprovedTestimonialsPublic));
app.get("/team", wrap(getTeamPublic));
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));
@@ -156,6 +214,28 @@ app.post("/admin/notes/delete", requireTeamSession, withSession(deleteNote));
app.post("/admin/drafts/list", requireTeamSession, withSession(listDrafts)); app.post("/admin/drafts/list", requireTeamSession, withSession(listDrafts));
app.post("/admin/drafts/save", requireTeamSession, withSession(saveDraft)); app.post("/admin/drafts/save", requireTeamSession, withSession(saveDraft));
// Startseite: "Event des Jahres" (20.08.2026)
app.post("/admin/events/list", requireTeamSession, withSession(getEventsAdmin));
app.post("/admin/events/save", requireTeamSession, withSession(saveEvent));
app.post("/admin/events/upload-image", requireTeamSession, bildUpload.single("image"), withSession(uploadEventImage));
// Spezielles Live-Event mit Countdown auf streamplan.html (20.08.2026)
app.post("/admin/special-event", requireTeamSession, withSession(getSpecialEventAdmin));
app.post("/admin/special-event/save", requireTeamSession, withSession(saveSpecialEvent));
// "Stimmen"-Warteschlange (20.08.2026)
app.post("/admin/testimonials/list", requireTeamSession, withSession(getTestimonialsAdmin));
app.post("/admin/testimonials/status", requireTeamSession, withSession(setTestimonialStatus));
app.post("/admin/testimonials/delete", requireTeamSession, withSession(deleteTestimonial));
// Team-Mitglieder (Modis/Scouts/Creator/Manager) — 21.08.2026
app.post("/admin/team/list", requireTeamSession, withSession(getTeamAdmin));
app.post("/admin/team/save", requireTeamSession, withSession(saveTeamMember));
app.post("/admin/team/import", requireTeamSession, withSession(importLegacyMember));
app.post("/admin/team/delete", requireTeamSession, withSession(deleteTeamMember));
app.post("/admin/team/upload-photo", requireTeamSession, teamBildUpload.single("image"), withSession(uploadTeamPhoto));
app.post("/admin/site-texts/save", requireTeamSession, withSession(saveSiteText));
app.post("/admin/roles/list", requireTeamSession, withSession(listRoles)); app.post("/admin/roles/list", requireTeamSession, withSession(listRoles));
app.post("/admin/roles/create", requireTeamSession, withSession(createRole)); app.post("/admin/roles/create", requireTeamSession, withSession(createRole));
app.post("/admin/roles/update", requireTeamSession, withSession(updateRole)); app.post("/admin/roles/update", requireTeamSession, withSession(updateRole));
@@ -185,6 +265,13 @@ app.post("/admin/supporter/premium/update", requireTeamSession, withSession(upda
app.post("/admin/supporter/collections/list", requireTeamSession, withSession(listCollections)); app.post("/admin/supporter/collections/list", requireTeamSession, withSession(listCollections));
app.post("/admin/supporter/collections/upsert", requireTeamSession, withSession(upsertCollection)); app.post("/admin/supporter/collections/upsert", requireTeamSession, withSession(upsertCollection));
// Supporter-Abstimmungen (20.08.2026)
app.post("/admin/polls/list", requireTeamSession, withSession(listPolls));
app.post("/admin/polls/create", requireTeamSession, withSession(createPoll));
app.post("/admin/polls/close", requireTeamSession, withSession(closePoll));
app.post("/admin/polls/reopen", requireTeamSession, withSession(reopenPoll));
app.post("/admin/polls/delete", requireTeamSession, withSession(deletePoll));
/* ---------- Öffentlicher Supporter-Bereich (eigene Session, requireSupporterAuth) ---------- */ /* ---------- Öffentlicher Supporter-Bereich (eigene Session, requireSupporterAuth) ---------- */
app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter)); app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter));
@@ -197,6 +284,8 @@ app.post("/supporter/delete-account", requireSupporterAuth, withSupporter(delete
app.post("/supporter/paypal/start", requireSupporterAuth, withSupporter(startPaypalSubscription)); app.post("/supporter/paypal/start", requireSupporterAuth, withSupporter(startPaypalSubscription));
app.post("/supporter/paypal/confirm", requireSupporterAuth, withSupporter(confirmPaypalSubscription)); app.post("/supporter/paypal/confirm", requireSupporterAuth, withSupporter(confirmPaypalSubscription));
app.post("/supporter/paypal/cancel", requireSupporterAuth, withSupporter(cancelPaypalSubscription)); app.post("/supporter/paypal/cancel", requireSupporterAuth, withSupporter(cancelPaypalSubscription));
app.post("/supporter/polls/active", requireSupporterAuth, withSupporter(getActivePollPublic));
app.post("/supporter/polls/vote", requireSupporterAuth, withSupporter(voteOnPoll));
app.use((req, res) => json(res, { ok: false, error: "Not found." }, 404)); app.use((req, res) => json(res, { ok: false, error: "Not found." }, 404));
@@ -212,6 +301,10 @@ app.use((err, req, res, next) => {
if (err?.type === "entity.too.large") { if (err?.type === "entity.too.large") {
return json(res, { ok: false, error: "Die Anfrage ist zu groß." }, 413); return json(res, { ok: false, error: "Die Anfrage ist zu groß." }, 413);
} }
if (err instanceof multer.MulterError) {
const nachricht = err.code === "LIMIT_FILE_SIZE" ? "Das Bild ist zu groß (maximal 5 MB)." : "Bild-Upload fehlgeschlagen.";
return json(res, { ok: false, error: nachricht }, 400);
}
console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err); console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
if (res.headersSent) return; if (res.headersSent) return;
return json(res, { ok: false, error: "Interner Fehler." }, 500); return json(res, { ok: false, error: "Interner Fehler." }, 500);
+9 -1
View File
@@ -45,6 +45,11 @@ export const PERMISSIONS = {
SUPPORTER_VIEW: "Supporter-Abonnenten ansehen", SUPPORTER_VIEW: "Supporter-Abonnenten ansehen",
SUPPORTER_MANAGE_PREMIUMS: "Supporter-Prämien & Versand verwalten", SUPPORTER_MANAGE_PREMIUMS: "Supporter-Prämien & Versand verwalten",
SUPPORTER_MANAGE_COLLECTIONS: "Supporter-Kollektionen verwalten", SUPPORTER_MANAGE_COLLECTIONS: "Supporter-Kollektionen verwalten",
POLLS_MANAGE: "Supporter-Abstimmungen erstellen & auswerten",
EVENTS_MANAGE: "Startseite: \"Event des Jahres\" verwalten",
TESTIMONIALS_MANAGE: "Stimmen/Testimonials verwalten",
TEAM_MANAGE: "Team-Mitglieder verwalten (Modis/Scouts/Creator/Manager)",
}; };
export const PERMISSION_GROUPS = [ export const PERMISSION_GROUPS = [
@@ -54,7 +59,10 @@ export const PERMISSION_GROUPS = [
{ label: "Interne Notizen", keys: ["NOTES_READ", "NOTES_WRITE", "NOTES_EDIT_OWN", "NOTES_EDIT_OTHERS", "NOTES_DELETE"] }, { label: "Interne Notizen", keys: ["NOTES_READ", "NOTES_WRITE", "NOTES_EDIT_OWN", "NOTES_EDIT_OTHERS", "NOTES_DELETE"] },
{ label: "Teamverwaltung", keys: ["TEAM_VIEW", "TEAM_PREPARE_USER", "TEAM_LOCK_USER", "TEAM_ASSIGN_ROLE", "TEAM_CREATE_ROLE", "TEAM_EDIT_PERMISSIONS"] }, { label: "Teamverwaltung", keys: ["TEAM_VIEW", "TEAM_PREPARE_USER", "TEAM_LOCK_USER", "TEAM_ASSIGN_ROLE", "TEAM_CREATE_ROLE", "TEAM_EDIT_PERMISSIONS"] },
{ label: "Einstellungen", keys: ["SETTINGS_EDIT_FORMS", "SETTINGS_EDIT_STATUS_TYPES", "SETTINGS_VIEW_STATS", "SETTINGS_VIEW_ACTIVITY_LOG", "SETTINGS_EDIT_GENERAL"] }, { label: "Einstellungen", keys: ["SETTINGS_EDIT_FORMS", "SETTINGS_EDIT_STATUS_TYPES", "SETTINGS_VIEW_STATS", "SETTINGS_VIEW_ACTIVITY_LOG", "SETTINGS_EDIT_GENERAL"] },
{ label: "DogiCrew 🩵", keys: ["SUPPORTER_VIEW", "SUPPORTER_MANAGE_PREMIUMS", "SUPPORTER_MANAGE_COLLECTIONS"] }, { label: "DogiCrew 🩵", keys: ["SUPPORTER_VIEW", "SUPPORTER_MANAGE_PREMIUMS", "SUPPORTER_MANAGE_COLLECTIONS", "POLLS_MANAGE"] },
{ label: "Startseite", keys: ["EVENTS_MANAGE"] },
{ label: "Stimmen", keys: ["TESTIMONIALS_MANAGE"] },
{ label: "Team", keys: ["TEAM_MANAGE"] },
]; ];
export const OWNER_ONLY_ACTIONS = [ export const OWNER_ONLY_ACTIONS = [
+58
View File
@@ -0,0 +1,58 @@
/* =====================================================================
lib/translate.js — automatische Übersetzung für admin-gepflegte Texte
(aktuell nur "Event des Jahres" auf der Startseite, 20.08.2026).
Bewusste Entscheidung (Nutzer-Rückfrage 20.08.2026): Dogi tippt Titel/Text
nur auf Deutsch in die Verwaltung ein, die anderen Sprachen entstehen
automatisch beim Speichern — NICHT die sonst auf der ganzen Seite geltende
Regel "immer echte, geprüfte Übersetzungen", sondern eine bewusste
Ausnahme für diesen einen admin-gepflegten Bereich. Maschinen-Übersetzung
ist NIE so gut wie eine echte — für schnell änderbare Startseiten-Teaser
akzeptabel, für Rechtstexte/feste Seiteninhalte weiterhin NICHT verwenden.
de-CH bekommt bewusst NICHT automatisch übersetzt: der Website-Dialekt
(Solothurn/Zürich-nahe Schreibweise, siehe main.js-Kommentar zu I18N_CORE)
ist keine von Übersetzungs-APIs unterstützte Zielsprache — eine
automatische "Übersetzung" nach de-CH würde bestenfalls Standarddeutsch
zurückgeben, das wäre nicht ehrlich als "Dialekt" beschriftet. de-CH
bekommt stattdessen exakt denselben Text wie de.
MyMemory (https://mymemory.translated.net) statt Google/DeepL: völlig
kostenlos, kein API-Key/Anmeldung nötig (passt zu "Kosten möglichst 0€"),
für kurze Teaser-Texte ausreichende Qualität. Absichtlich KEIN lokal
gehosteter Übersetzungsdienst (z.B. LibreTranslate) — bei der seltenen
Nutzung (2 kurze Texte, gelegentlich geändert) wäre ein dauerhaft
laufender zusätzlicher Dienst auf dem ohnehin schon mehrfach belegten
Server unverhältnismäßig viel Aufwand für den tatsächlichen Bedarf. */
const SPRACHEN_ZIEL = ["en", "fr", "pt"];
async function uebersetzeEinzeln(text, ziel) {
if (!text || !text.trim()) return "";
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=de|${ziel}`;
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
if (!res.ok) throw new Error(`Übersetzungsdienst antwortete mit ${res.status}`);
const data = await res.json();
const uebersetzt = data?.responseData?.translatedText;
if (!uebersetzt || typeof uebersetzt !== "string") throw new Error("Keine Übersetzung erhalten.");
return uebersetzt;
}
/** Übersetzt einen deutschen Text nach en/fr/pt, de-CH = 1:1 Kopie von de.
Bei Fehlern (Dienst nicht erreichbar etc.) fällt die jeweilige Sprache
auf den deutschen Text zurück, statt die ganze Speicherung scheitern zu
lassen — "fail closed" mit sichtbarem Ergebnis statt kaputter Anzeige. */
export async function uebersetzeAlleSprachen(textDe) {
const ergebnis = { de: textDe || "", "de-CH": textDe || "" };
await Promise.all(
SPRACHEN_ZIEL.map(async (ziel) => {
try {
ergebnis[ziel] = await uebersetzeEinzeln(textDe, ziel);
} catch (err) {
console.error(`[translate] ${ziel} fehlgeschlagen, falle auf Deutsch zurück:`, err?.message || err);
ergebnis[ziel] = textDe || "";
}
})
);
return ergebnis;
}
+1
View File
@@ -14,6 +14,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"multer": "^1.4.5-lts.1",
"node-cron": "^3.0.3" "node-cron": "^3.0.3"
} }
} }
+114
View File
@@ -0,0 +1,114 @@
/* =====================================================================
routes/events.js — "Event des Jahres" auf der Startseite (index.html),
admin-pflegbar aus der Verwaltung heraus (Nutzer-Wunsch 20.08.2026:
"will ich in der admin seite das selbst gestalten können, mit bild und
text und am besten auch einen link"). Zeigt IMMER genau 2 feste Slots
(1 und 2) — kein wachsender Katalog, deshalb genügt die bereits
bestehende app_settings-Tabelle (key/value) statt einer eigenen Tabelle.
Öffentlicher Lesezugriff (getEventsPublic) ohne Session, damit
index.html die Inhalte für jede Besucherin/jeden Besucher laden kann —
Schreibzugriff nur mit EVENTS_MANAGE-Berechtigung (Owner immer erlaubt,
siehe can()). ===================================================================== */
import { db } from "../db.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
function settingsKey(slot) {
return `event_des_jahres_${slot}`;
}
function ladeSlot(slot) {
const row = db.prepare(`SELECT value FROM app_settings WHERE key = ?`).get(settingsKey(slot));
if (!row) return null;
try {
return JSON.parse(row.value);
} catch {
return null;
}
}
/** Öffentlich, keine Session: liefert nur AKTIVIERTE Slots (Nutzer-Wunsch
20.08.2026: "die kisten sollen leer sein solang wie ich nichts rein
setze, am besten sollen die sogar immer nur erscheinen wenn ich was
rein setze und sie aktiviere") -- ein Slot mit gespeichertem Inhalt,
aber ohne gesetztes "aktiv", bleibt für Besucher:innen unsichtbar
(Entwurfsmodus), obwohl er in der Verwaltung längst vorausgefüllt ist. */
export async function getEventsPublic(req, res) {
const gibFreiWennAktiv = (slot) => {
const eintrag = ladeSlot(slot);
return eintrag && eintrag.aktiv ? eintrag : null;
};
return json(res, { ok: true, events: [gibFreiWennAktiv(1), gibFreiWennAktiv(2)] });
}
/** Für die Verwaltung: liefert BEIDE Slots roh (auch inaktive/Entwürfe),
damit das Formular beim Öffnen alles vorausfüllen kann. */
export async function getEventsAdmin(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
return json(res, { ok: true, events: [ladeSlot(1), ladeSlot(2)] });
}
/** NUR Bild-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware). */
export async function uploadEventImage(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400);
const url = `/uploads/events/${req.file.filename}`;
logAction(session.actor, "events.image_uploaded", req.file.filename, null);
return json(res, { ok: true, url });
}
export async function saveEvent(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
const body = req.body || {};
const slot = Number(body.slot);
if (slot !== 1 && slot !== 2) return json(res, { ok: false, error: "Ungültiger Slot (nur 1 oder 2)." }, 400);
const datum = String(body.datum || "").trim().slice(0, 80);
const titelDe = String(body.titel || "").trim().slice(0, 120);
const textDe = String(body.text || "").trim().slice(0, 400);
const link = String(body.link || "").trim().slice(0, 300) || null;
const bild = String(body.bild || "").trim().slice(0, 300) || null;
const aktiv = !!body.aktiv;
if (!titelDe || !textDe) {
return json(res, { ok: false, error: "Titel und Text sind Pflicht." }, 400);
}
// Nur echte http(s)-Links zulassen (kein javascript:-Link etc.)
if (link && !/^https?:\/\//i.test(link)) {
return json(res, { ok: false, error: "Link muss mit http:// oder https:// beginnen." }, 400);
}
// Aktivieren nur mit vollständigem Pflichtinhalt möglich -- verhindert ein
// leeres/kaputtes, aber "aktives" Feld auf der Startseite.
if (aktiv && (!titelDe || !textDe)) {
return json(res, { ok: false, error: "Zum Aktivieren müssen Titel und Text ausgefüllt sein." }, 400);
}
const [titel, text] = await Promise.all([uebersetzeAlleSprachen(titelDe), uebersetzeAlleSprachen(textDe)]);
const eintrag = {
datum, titel, text, link, bild, aktiv,
aktualisiertVon: session.actor,
aktualisiertAm: new Date().toISOString(),
};
db.prepare(
`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
).run(settingsKey(slot), JSON.stringify(eintrag), eintrag.aktualisiertAm);
logAction(session.actor, "events.saved", String(slot), { titel: titelDe });
return json(res, { ok: true, event: eintrag });
}
+178
View File
@@ -0,0 +1,178 @@
/* =====================================================================
routes/polls.js — Supporter-Abstimmungen (20.08.2026), "nächste
Ausbaustufe" aus Dogfather_VanVan_Supporter_Abo.odt Abschnitt 20.
Admin-Seite (verwaltung.html): Frage + Antwortoptionen auf Deutsch
erstellen (POLLS_MANAGE-Recht, Owner immer erlaubt), automatische
Übersetzung wie bei "Event des Jahres" (lib/translate.js), Ergebnisse
live einsehen, Abstimmung schließen oder löschen.
Supporter-Bereich: genau EINE aktive Abstimmung wird angezeigt (die
zuletzt erstellte), einmal Stimme abgeben (UNIQUE(poll_id, supporter_id)
in der Datenbank verhindert doppelte Stimmen robust, auch bei
gleichzeitigen Anfragen — keine reine Anwendungslogik-Prüfung).
===================================================================== */
import { db } from "../db.js";
import { generateId, nowIso } from "../lib/crypto.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
const MIN_OPTIONEN = 2;
const MAX_OPTIONEN = 6;
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
function parsePoll(row) {
return {
id: row.id,
question: JSON.parse(row.question),
options: JSON.parse(row.options),
status: row.status,
createdAt: row.created_at,
createdBy: row.created_by,
closedAt: row.closed_at,
};
}
function voteCountsFor(pollId, optionCount) {
const rows = db
.prepare(`SELECT option_index, COUNT(*) as n FROM supporter_poll_votes WHERE poll_id = ? GROUP BY option_index`)
.all(pollId);
const counts = new Array(optionCount).fill(0);
for (const r of rows) {
if (r.option_index >= 0 && r.option_index < optionCount) counts[r.option_index] = r.n;
}
return counts;
}
/* ---------- Admin (interne Team-Session, POLLS_MANAGE) ---------- */
export async function createPoll(req, res, session) {
if (!can(session, "POLLS_MANAGE")) return forbidden(res);
const body = req.body || {};
const fragestellungDe = String(body.frage || "").trim().slice(0, 200);
const optionenDe = Array.isArray(body.optionen)
? body.optionen.map((o) => String(o || "").trim().slice(0, 100)).filter(Boolean)
: [];
if (!fragestellungDe) return json(res, { ok: false, error: "Bitte eine Frage eingeben." }, 400);
if (optionenDe.length < MIN_OPTIONEN) {
return json(res, { ok: false, error: `Mindestens ${MIN_OPTIONEN} Antwortoptionen nötig.` }, 400);
}
if (optionenDe.length > MAX_OPTIONEN) {
return json(res, { ok: false, error: `Höchstens ${MAX_OPTIONEN} Antwortoptionen erlaubt.` }, 400);
}
const question = await uebersetzeAlleSprachen(fragestellungDe);
const options = await Promise.all(optionenDe.map((o) => uebersetzeAlleSprachen(o)));
const id = generateId();
const jetzt = nowIso();
db.prepare(
`INSERT INTO supporter_polls (id, question, options, status, created_at, created_by)
VALUES (?, ?, ?, 'active', ?, ?)`
).run(id, JSON.stringify(question), JSON.stringify(options), jetzt, session.actor);
logAction(session.actor, "polls.created", id, { frage: fragestellungDe });
return json(res, { ok: true, poll: { id, question, options, status: "active", createdAt: jetzt, closedAt: null } });
}
export async function listPolls(req, res, session) {
if (!can(session, "POLLS_MANAGE")) return forbidden(res);
const rows = db.prepare(`SELECT * FROM supporter_polls ORDER BY created_at DESC`).all();
const polls = rows.map((row) => {
const p = parsePoll(row);
const counts = voteCountsFor(p.id, p.options.length);
return { ...p, counts, totalVotes: counts.reduce((a, b) => a + b, 0) };
});
return json(res, { ok: true, polls });
}
export async function closePoll(req, res, session) {
if (!can(session, "POLLS_MANAGE")) return forbidden(res);
const id = String(req.body?.pollId || "");
const row = db.prepare(`SELECT id, status FROM supporter_polls WHERE id = ?`).get(id);
if (!row) return json(res, { ok: false, error: "Abstimmung nicht gefunden." }, 404);
db.prepare(`UPDATE supporter_polls SET status = 'closed', closed_at = ? WHERE id = ?`).run(nowIso(), id);
logAction(session.actor, "polls.closed", id, null);
return json(res, { ok: true });
}
export async function reopenPoll(req, res, session) {
if (!can(session, "POLLS_MANAGE")) return forbidden(res);
const id = String(req.body?.pollId || "");
const row = db.prepare(`SELECT id FROM supporter_polls WHERE id = ?`).get(id);
if (!row) return json(res, { ok: false, error: "Abstimmung nicht gefunden." }, 404);
db.prepare(`UPDATE supporter_polls SET status = 'active', closed_at = NULL WHERE id = ?`).run(id);
logAction(session.actor, "polls.reopened", id, null);
return json(res, { ok: true });
}
export async function deletePoll(req, res, session) {
if (!can(session, "POLLS_MANAGE")) return forbidden(res);
const id = String(req.body?.pollId || "");
db.prepare(`DELETE FROM supporter_poll_votes WHERE poll_id = ?`).run(id);
const info = db.prepare(`DELETE FROM supporter_polls WHERE id = ?`).run(id);
if (info.changes === 0) return json(res, { ok: false, error: "Abstimmung nicht gefunden." }, 404);
logAction(session.actor, "polls.deleted", id, null);
return json(res, { ok: true });
}
/* ---------- Supporter-Bereich (eigene Session, jeder aktive Supporter) ---------- */
export async function getActivePollPublic(req, res, supporter) {
const row = db.prepare(`SELECT * FROM supporter_polls WHERE status = 'active' ORDER BY created_at DESC LIMIT 1`).get();
if (!row) return json(res, { ok: true, poll: null });
const poll = parsePoll(row);
const meineStimme = db
.prepare(`SELECT option_index FROM supporter_poll_votes WHERE poll_id = ? AND supporter_id = ?`)
.get(poll.id, supporter.id);
const antwort = { ok: true, poll: { id: poll.id, question: poll.question, options: poll.options }, myVote: meineStimme ? meineStimme.option_index : null };
// Ergebnisse nur zeigen, wenn schon abgestimmt wurde (sonst könnte das
// Ergebnis die eigene Stimme beeinflussen) — dieselbe Logik wie bei
// öffentlichen Umfrage-Tools üblich.
if (meineStimme) {
const counts = voteCountsFor(poll.id, poll.options.length);
antwort.counts = counts;
antwort.totalVotes = counts.reduce((a, b) => a + b, 0);
}
return json(res, antwort);
}
export async function voteOnPoll(req, res, supporter) {
const pollId = String(req.body?.pollId || "");
const optionIndex = Number(req.body?.optionIndex);
const poll = db.prepare(`SELECT * FROM supporter_polls WHERE id = ? AND status = 'active'`).get(pollId);
if (!poll) return json(res, { ok: false, error: "Diese Abstimmung ist nicht mehr aktiv." }, 404);
const options = JSON.parse(poll.options);
if (!Number.isInteger(optionIndex) || optionIndex < 0 || optionIndex >= options.length) {
return json(res, { ok: false, error: "Ungültige Antwortoption." }, 400);
}
try {
db.prepare(
`INSERT INTO supporter_poll_votes (id, poll_id, supporter_id, option_index, voted_at) VALUES (?, ?, ?, ?, ?)`
).run(generateId(), pollId, supporter.id, optionIndex, nowIso());
} catch (err) {
// UNIQUE(poll_id, supporter_id) verhindert Doppelstimmen — sauber statt
// als Serverfehler durchzureichen.
if (String(err?.message || "").includes("UNIQUE")) {
return json(res, { ok: false, error: "Du hast bei dieser Abstimmung bereits mitgemacht." }, 409);
}
throw err;
}
logAction("system", "polls.voted", pollId, { supporterId: supporter.id, optionIndex });
const counts = voteCountsFor(pollId, options.length);
return json(res, { ok: true, counts, totalVotes: counts.reduce((a, b) => a + b, 0) });
}
+95
View File
@@ -0,0 +1,95 @@
/* =====================================================================
routes/site-texts.js — admin-editierbare Kategorie-Namen & Seitentitel
(21.08.2026, Nutzer-Wunsch: "ich will ich titel und namen von
kategorien und seiten ändern können in der verwaltung").
Bewusst als generischer key->{de,"de-CH",en,fr,pt}-Override gebaut
(wiederverwendet die vorhandene app_settings-Tabelle, key-Präfix
"site_text_"), NICHT als eigene Tabelle -- dieselbe Technik wie
"Event des Jahres". Ein fester, serverseitiger Katalog erlaubter
Keys (ERLAUBTE_KEYS) verhindert, dass beliebige/kaputte Keys
gespeichert werden können. Aktuell nur die vier Team-Kategorien +
ihre Seitentitel belegt -- das Muster ist aber bewusst so gebaut,
dass in einer künftigen Runde weitere Seiten/Überschriften einfach
per neuem Key ergänzt werden können (siehe main.js:
applyTranslations() prüft data-site-text-key genau wie data-i18n).
Öffentlich lesbar (jede Seite braucht das ohne Login), Schreiben nur
mit TEAM_MANAGE (dieselbe Berechtigung wie die Team-Verwaltung,
bewusst keine eigene -- beides gehört zusammen). ===================================================================== */
import { db } from "../db.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
// key -> { label, maxLen } -- label ist nur für Log-/Fehlermeldungen.
const ERLAUBTE_KEYS = {
kategorie_modi: { label: "Kategorie-Name: Modi", maxLen: 40 },
kategorie_scout: { label: "Kategorie-Name: Scout", maxLen: 40 },
kategorie_creator: { label: "Kategorie-Name: Creator", maxLen: 40 },
kategorie_manager: { label: "Kategorie-Name: Manager", maxLen: 40 },
seitentitel_modi: { label: "Seitentitel: Modi-Team", maxLen: 100 },
seitentitel_scout: { label: "Seitentitel: Scout-Team", maxLen: 100 },
seitentitel_creator: { label: "Seitentitel: Creator", maxLen: 100 },
seitentitel_manager: { label: "Seitentitel: Manager", maxLen: 100 },
};
function settingsKey(key) {
return `site_text_${key}`;
}
/** Öffentlich, keine Session: liefert alle aktuell GESETZTEN Overrides
als { key: {de,"de-CH",en,fr,pt} }. Keys ohne eigenen Override
fehlen einfach im Ergebnis -- die aufrufende Seite behält dann ihren
eingebauten Standardtext (siehe main.js). */
export async function getSiteTextsPublic(req, res) {
const rows = db
.prepare(`SELECT key, value FROM app_settings WHERE key LIKE 'site_text_%'`)
.all();
const texte = {};
for (const row of rows) {
const kurzKey = row.key.replace(/^site_text_/, "");
try {
texte[kurzKey] = JSON.parse(row.value);
} catch {
/* kaputter Eintrag -> einfach überspringen, kein Fehler für alle anderen Keys */
}
}
return json(res, { ok: true, texte });
}
export async function saveSiteText(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const key = String(body.key || "").trim();
const eintrag = ERLAUBTE_KEYS[key];
if (!eintrag) return json(res, { ok: false, error: "Unbekannter Text-Schlüssel." }, 400);
const textDe = String(body.text || "").trim().slice(0, eintrag.maxLen);
if (!textDe) {
// Leeres Feld = Override entfernen -> Seite fällt zurück auf ihren
// eingebauten Standardtext (Dogi kann so jederzeit "zurücksetzen").
db.prepare(`DELETE FROM app_settings WHERE key = ?`).run(settingsKey(key));
logAction(session.actor, "site_text.reset", key, null);
return json(res, { ok: true, text: null });
}
const text = await uebersetzeAlleSprachen(textDe);
db.prepare(
`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
).run(settingsKey(key), JSON.stringify(text), new Date().toISOString());
logAction(session.actor, "site_text.saved", key, { textDe });
return json(res, { ok: true, text });
}
+97
View File
@@ -0,0 +1,97 @@
/* =====================================================================
routes/special-event.js — "Spezielles Live-Event" für streamplan.html
(20.08.2026, Nutzer-Wunsch: "sehr wichtig ist dass ich auch spezielle
live events da auch eintragen kann in der verwaltungsseite so dass da
eine zweite uhr erscheint wo die zeit dann für dieses spezielle event
läuft"). Anders als "Event des Jahres" (2 feste Slots) gibt es hier
GENAU EIN Event mit einem exakten Zieldatum+Uhrzeit statt nur einem
freien Datumstext — daraus lässt sich ein echter Countdown berechnen.
Läuft auf derselben app_settings-Tabelle (key/value), kein Schema-Umbau
nötig. Gleiche Rechte-/Freigabe-Logik wie "Event des Jahres"
(EVENTS_MANAGE, Owner immer erlaubt, öffentlich lesbar, aber nur wenn
aktiviert UND das Zieldatum noch in der Zukunft liegt). ===================================================================== */
import { db } from "../db.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
const SETTINGS_KEY = "special_live_event";
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
function laden() {
const row = db.prepare(`SELECT value FROM app_settings WHERE key = ?`).get(SETTINGS_KEY);
if (!row) return null;
try {
return JSON.parse(row.value);
} catch {
return null;
}
}
/** Öffentlich, keine Session: nur wenn aktiviert UND das Zieldatum noch
nicht vorbei ist (ein abgelaufenes Event soll nicht ewig als "Countdown
bei 00:00:00" hängen bleiben, sondern einfach verschwinden). */
export async function getSpecialEventPublic(req, res) {
const eintrag = laden();
const istGueltig = eintrag && eintrag.aktiv && new Date(eintrag.zieldatum) > new Date();
return json(res, { ok: true, event: istGueltig ? eintrag : null });
}
/** Für die Verwaltung: liefert den rohen Stand (auch inaktiv/abgelaufen),
damit das Formular beim Öffnen alles vorausfüllen kann. */
export async function getSpecialEventAdmin(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
return json(res, { ok: true, event: laden() });
}
export async function saveSpecialEvent(req, res, session) {
if (!can(session, "EVENTS_MANAGE")) return forbidden(res);
const body = req.body || {};
const titelDe = String(body.titel || "").trim().slice(0, 120);
const zieldatumRoh = String(body.zieldatum || "").trim();
const link = String(body.link || "").trim().slice(0, 300) || null;
const aktiv = !!body.aktiv;
if (!titelDe) return json(res, { ok: false, error: "Titel ist Pflicht." }, 400);
const zieldatum = new Date(zieldatumRoh);
if (!zieldatumRoh || Number.isNaN(zieldatum.getTime())) {
return json(res, { ok: false, error: "Bitte ein gültiges Datum + Uhrzeit angeben." }, 400);
}
if (link && !/^https?:\/\//i.test(link)) {
return json(res, { ok: false, error: "Link muss mit http:// oder https:// beginnen." }, 400);
}
if (aktiv && zieldatum <= new Date()) {
return json(res, { ok: false, error: "Das Zieldatum liegt in der Vergangenheit — zum Aktivieren muss es in der Zukunft liegen." }, 400);
}
const titel = await uebersetzeAlleSprachen(titelDe);
const eintrag = {
titel,
zieldatum: zieldatum.toISOString(),
link,
aktiv,
aktualisiertVon: session.actor,
aktualisiertAm: new Date().toISOString(),
};
db.prepare(
`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
).run(SETTINGS_KEY, JSON.stringify(eintrag), eintrag.aktualisiertAm);
logAction(session.actor, "special_event.saved", null, { titel: titelDe, zieldatum: eintrag.zieldatum, aktiv });
return json(res, { ok: true, event: eintrag });
}
+368
View File
@@ -0,0 +1,368 @@
/* =====================================================================
routes/team.js — Team-Mitglieder-Verwaltung (21.08.2026, Nutzer-Wunsch:
"überall wo Modis sind oder Scouts oder Manager, oder Creator, will ich
dass ich die easy über meine Verwaltungsseite hinzufüge und die
automatisch in der Website hinzugefügt werden ... auch mit Fotos und
TikTok Link").
Update 21.08.2026 (Runde 2, Nutzer-Wunsch: "wenn ich eine änderung
mache ist es auch für jeden für die die schon da sind und die neuen"):
volle Feld-Parität mit den von Hand gepflegten Profilen (intro/bioHtml/
extraCta) PLUS ein Import-Weg für bestehende Profile (importLegacyMember)
-- danach lässt sich JEDES Mitglied (alt oder neu) hier bearbeiten,
und die Website zeigt (siehe window.dogiTeamLaden() in main.js) für
importierte slugs die DB-Version statt der statischen an.
WICHTIG: importLegacyMember übersetzt NICHT automatisch -- die
bestehenden Profile haben von Hand geschriebene, geprüfte Übersetzungen
in allen 5 Sprachen (siehe data-modis.js etc.), die dürfen beim Import
nicht durch Maschinenübersetzung ersetzt werden. saveTeamMember (Neu
anlegen/Bearbeiten in der Verwaltung) übersetzt weiterhin automatisch,
weil dort nur Deutsch eingetippt wird.
Öffentlicher Lesezugriff (getTeamPublic) ohne Session, nur AKTIVE
Mitglieder -- Schreibzugriff nur mit TEAM_MANAGE-Berechtigung.
===================================================================== */
import { db } from "../db.js";
import { generateId, nowIso } from "../lib/crypto.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
const KATEGORIEN = ["modi", "scout", "creator", "manager"];
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
function parseJsonFeld(text) {
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
/** Prüft, ob ein vom Client geschicktes mehrsprachiges Feld wirklich ein
Objekt mit (mindestens) einem "de"-Text ist -- genutzt vom Import, der
KEINE eigene Übersetzung anstößt und deshalb ein vollständiges Objekt
erwartet, kein einzelnes Deutsch-Textfeld. */
function istGueltigesSprachFeld(x) {
return x && typeof x === "object" && typeof x.de === "string" && x.de.trim().length > 0;
}
/** Baut aus einem client-gesendeten mehrsprachigen Feld ein sauberes,
auf die bekannten 5 Sprachen begrenztes Objekt (verhindert, dass
beliebige zusätzliche Keys in die Datenbank gelangen). */
function saubereSprachen(x, maxLen) {
const out = {};
for (const sprache of SPRACHEN) {
out[sprache] = String(x?.[sprache] ?? x?.de ?? "").slice(0, maxLen);
}
return out;
}
/** Slug aus dem Namen ableiten (Kleinbuchstaben, Umlaute vereinfacht,
alles außer a-z0-9 wird zu "-"), falls die Verwaltung keinen eigenen
Slug mitschickt -- selbes Muster wie an anderen Stellen der Seite. */
function slugAusName(name) {
return String(name || "")
.toLowerCase()
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 60);
}
function slugFrei(slug, ausgenommeneId) {
const row = db.prepare(`SELECT id FROM team_members WHERE slug = ?`).get(slug);
return !row || row.id === ausgenommeneId;
}
/** Macht einen Slug bei Kollision automatisch eindeutig ("-2", "-3", ...)
statt hart abzulehnen -- Dogi/VanVan sollen nicht wegen eines
Namensdopplers (z.B. zwei Personen mit Spitzname "Foxy") hängen
bleiben. */
function eindeutigerSlug(wunschSlug, ausgenommeneId) {
if (slugFrei(wunschSlug, ausgenommeneId)) return wunschSlug;
let zaehler = 2;
while (!slugFrei(`${wunschSlug}-${zaehler}`, ausgenommeneId)) zaehler++;
return `${wunschSlug}-${zaehler}`;
}
function oeffentlichesFeld(row) {
const extraCtaLabel = parseJsonFeld(row.extra_cta_label_json);
return {
id: row.id,
kategorie: row.kategorie,
name: row.name,
slug: row.slug,
rolle: parseJsonFeld(row.rolle_json),
foto: row.foto_url || "",
tiktok: row.tiktok_url || "",
intro: parseJsonFeld(row.intro_json),
bio: parseJsonFeld(row.bio_json),
bioHtml: parseJsonFeld(row.bio_html_json),
zitat: parseJsonFeld(row.zitat_json),
rang: row.rang,
rangLabel: parseJsonFeld(row.rang_label_json),
extraCta: row.extra_cta_href && extraCtaLabel ? { href: row.extra_cta_href, label: extraCtaLabel } : null,
};
}
function vollesFeld(row) {
return {
...oeffentlichesFeld(row),
status: row.status,
reihenfolge: row.reihenfolge,
erstelltAm: row.erstellt_am,
geaendertAm: row.geaendert_am,
};
}
/** Öffentlich, keine Session: nur AKTIVE Mitglieder. Ohne ?kategorie=
kommen alle vier Kategorien zusammen zurück (genutzt von profil.html,
das vorab nicht weiß, zu welcher Kategorie ein slug gehört). */
export async function getTeamPublic(req, res) {
const kategorie = String(req.query?.kategorie || "").trim();
let rows;
if (kategorie && KATEGORIEN.includes(kategorie)) {
rows = db
.prepare(`SELECT * FROM team_members WHERE kategorie = ? AND status = 'aktiv' ORDER BY reihenfolge ASC, erstellt_am ASC`)
.all(kategorie);
} else {
rows = db
.prepare(`SELECT * FROM team_members WHERE status = 'aktiv' ORDER BY reihenfolge ASC, erstellt_am ASC`)
.all();
}
return json(res, { ok: true, mitglieder: rows.map(oeffentlichesFeld) });
}
/** Für die Verwaltung: ALLE Mitglieder aller Kategorien/Status. */
export async function getTeamAdmin(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const rows = db.prepare(`SELECT * FROM team_members ORDER BY kategorie ASC, reihenfolge ASC, erstellt_am ASC`).all();
return json(res, { ok: true, mitglieder: rows.map(vollesFeld) });
}
/** NUR Foto-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware).
Volle Adresse statt relativem Pfad (Bugfix 21.08.2026, siehe derselbe Fehler +
Korrektur in routes/events.js/uploadEventImage): die Verwaltung UND die echte
Startseite laufen auf dogfather-universe.com (dogiweb-Dienst), die hochgeladene
Datei liegt aber tatsächlich unter postfach.dogfather-universe.com (dogiintern-
Dienst, siehe /uploads-Static-Route in index.js) -- ein relativer Pfad hätte
der Browser also fälschlich gegen die falsche Domain aufgelöst und ein
kaputtes Bild-Symbol gezeigt, sowohl in der Verwaltungs-Vorschau als auch auf
team-modis.html/team-scouts.html/creator.html/manager.html. */
export async function uploadTeamPhoto(req, res, session) {
if (!can(session, "TEAM_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/team/${req.file.filename}`;
logAction(session.actor, "team.photo_uploaded", req.file.filename, null);
return json(res, { ok: true, url });
}
export async function saveTeamMember(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const id = String(body.id || "").trim() || null;
const kategorie = String(body.kategorie || "").trim();
if (!KATEGORIEN.includes(kategorie)) {
return json(res, { ok: false, error: "Ungültige Kategorie." }, 400);
}
const name = String(body.name || "").trim().slice(0, 60);
if (!name) return json(res, { ok: false, error: "Name ist Pflicht." }, 400);
const rolleDe = String(body.rolle || "").trim().slice(0, 120);
if (!rolleDe) return json(res, { ok: false, error: "Rolle ist Pflicht." }, 400);
const wunschSlug = String(body.slug || "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 60) || slugAusName(name);
if (!wunschSlug) return json(res, { ok: false, error: "Konnte keinen gültigen Slug ableiten -- bitte Namen prüfen." }, 400);
const slug = eindeutigerSlug(wunschSlug, id);
const tiktokRoh = String(body.tiktok || "").trim().slice(0, 200);
let tiktokUrl = null;
if (tiktokRoh) {
if (/^https?:\/\//i.test(tiktokRoh)) {
tiktokUrl = tiktokRoh;
} else {
// Bequemlichkeit: reiner Benutzername (mit oder ohne @) wird
// automatisch zu einer vollen TikTok-URL ergänzt.
tiktokUrl = `https://www.tiktok.com/@${tiktokRoh.replace(/^@/, "")}`;
}
}
const introDe = String(body.intro || "").trim().slice(0, 400);
const bioDe = String(body.bio || "").trim().slice(0, 800);
const bioHtmlDe = String(body.bioHtml || "").trim().slice(0, 6000);
const zitatDe = String(body.zitat || "").trim().slice(0, 200);
const fotoUrl = String(body.foto || "").trim().slice(0, 300) || null;
const rang = body.rang !== undefined && body.rang !== null && body.rang !== ""
? Math.max(1, Math.min(99, Number(body.rang) || 0)) : null;
const rangLabelDe = String(body.rangLabel || "").trim().slice(0, 40);
const extraCtaHref = String(body.extraCtaHref || "").trim().slice(0, 300) || null;
const extraCtaLabelDe = String(body.extraCtaLabel || "").trim().slice(0, 60);
if (extraCtaHref && !/^https?:\/\//i.test(extraCtaHref) && !extraCtaHref.startsWith("/") && !extraCtaHref.endsWith(".html")) {
return json(res, { ok: false, error: "Zweiter Button: Link muss eine Seite (…html) oder http(s)-Adresse sein." }, 400);
}
const reihenfolge = Number.isFinite(Number(body.reihenfolge)) ? Number(body.reihenfolge) : 0;
const status = body.status === "pausiert" ? "pausiert" : "aktiv";
const [rolle, intro, bio, bioHtml, zitat, rangLabel, extraCtaLabel] = await Promise.all([
uebersetzeAlleSprachen(rolleDe),
introDe ? uebersetzeAlleSprachen(introDe) : Promise.resolve(null),
bioDe ? uebersetzeAlleSprachen(bioDe) : Promise.resolve(null),
bioHtmlDe ? uebersetzeAlleSprachen(bioHtmlDe) : Promise.resolve(null),
zitatDe ? uebersetzeAlleSprachen(zitatDe) : Promise.resolve(null),
rangLabelDe ? uebersetzeAlleSprachen(rangLabelDe) : Promise.resolve(null),
extraCtaLabelDe ? uebersetzeAlleSprachen(extraCtaLabelDe) : Promise.resolve(null),
]);
const jetzt = nowIso();
const werte = {
id: id || generateId(),
kategorie, name, slug,
rolle_json: JSON.stringify(rolle),
foto_url: fotoUrl,
tiktok_url: tiktokUrl,
intro_json: intro ? JSON.stringify(intro) : null,
bio_json: bio ? JSON.stringify(bio) : null,
bio_html_json: bioHtml ? JSON.stringify(bioHtml) : null,
zitat_json: zitat ? JSON.stringify(zitat) : null,
rang, rang_label_json: rangLabel ? JSON.stringify(rangLabel) : null,
extra_cta_href: extraCtaHref && extraCtaLabel ? extraCtaHref : null,
extra_cta_label_json: extraCtaHref && extraCtaLabel ? JSON.stringify(extraCtaLabel) : null,
reihenfolge, status,
geaendert_am: jetzt, geaendert_von: session.actor,
};
if (id) {
const info = db
.prepare(
`UPDATE team_members SET
kategorie=@kategorie, name=@name, slug=@slug, rolle_json=@rolle_json,
foto_url=@foto_url, tiktok_url=@tiktok_url, intro_json=@intro_json,
bio_json=@bio_json, bio_html_json=@bio_html_json, zitat_json=@zitat_json,
rang=@rang, rang_label_json=@rang_label_json,
extra_cta_href=@extra_cta_href, extra_cta_label_json=@extra_cta_label_json,
reihenfolge=@reihenfolge, status=@status,
geaendert_am=@geaendert_am, geaendert_von=@geaendert_von
WHERE id=@id`
)
.run(werte);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "team.updated", id, { name, kategorie });
} else {
db.prepare(
`INSERT INTO team_members
(id, kategorie, name, slug, rolle_json, foto_url, tiktok_url, intro_json,
bio_json, bio_html_json, zitat_json, rang, rang_label_json,
extra_cta_href, extra_cta_label_json, reihenfolge, status,
erstellt_am, geaendert_am, erstellt_von, geaendert_von)
VALUES
(@id, @kategorie, @name, @slug, @rolle_json, @foto_url, @tiktok_url, @intro_json,
@bio_json, @bio_html_json, @zitat_json, @rang, @rang_label_json,
@extra_cta_href, @extra_cta_label_json, @reihenfolge, @status,
@geaendert_am, @geaendert_am, @geaendert_von, @geaendert_von)`
).run(werte);
logAction(session.actor, "team.created", werte.id, { name, kategorie });
}
const row = db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(werte.id);
return json(res, { ok: true, mitglied: vollesFeld(row) });
}
/** Importiert ein BESTEHENDES, von Hand gepflegtes Profil (aus
data-modis.js/data-scouts.js/data-creator.js/data-manager.js) 1:1 in
die Datenbank -- danach ist es über die Verwaltung wie jedes andere
Mitglied bearbeitbar, und Änderungen gelten für ALLE (siehe
window.dogiTeamLaden()-Override-Logik in main.js).
WICHTIG: übersetzt NICHTS automatisch (anders als saveTeamMember) --
erwartet für jedes Textfeld ein bereits vollständiges {de,"de-CH",en,
fr,pt}-Objekt, wie es in den data-*.js-Dateien von Hand gepflegt
wird. Ist ein Feld kein gültiges Sprachobjekt, wird es einfach
weggelassen statt einen Fehler zu werfen -- ein unvollständiges
Altprofil soll den Import nicht komplett blockieren.
Idempotent: ist der slug schon vorhanden, passiert nichts (kein
Duplikat, kein Fehler) -- der Button in der Verwaltung kann beliebig
oft geklickt werden, ohne Doppel-Einträge zu erzeugen. */
export async function importLegacyMember(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const kategorie = String(body.kategorie || "").trim();
if (!KATEGORIEN.includes(kategorie)) return json(res, { ok: false, error: "Ungültige Kategorie." }, 400);
const name = String(body.name || "").trim().slice(0, 60);
const slug = String(body.slug || "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 60);
if (!name || !slug) return json(res, { ok: false, error: "Name und Slug sind Pflicht." }, 400);
if (!istGueltigesSprachFeld(body.rolle)) return json(res, { ok: false, error: "Rolle fehlt oder unvollständig." }, 400);
if (!slugFrei(slug, null)) {
return json(res, { ok: true, uebersprungen: true, grund: "Bereits importiert." });
}
const rolle = saubereSprachen(body.rolle, 120);
const intro = istGueltigesSprachFeld(body.intro) ? saubereSprachen(body.intro, 2000) : null;
const bio = istGueltigesSprachFeld(body.bio) ? saubereSprachen(body.bio, 800) : null;
const bioHtml = istGueltigesSprachFeld(body.bioHtml) ? saubereSprachen(body.bioHtml, 8000) : null;
const zitat = istGueltigesSprachFeld(body.zitat) ? saubereSprachen(body.zitat, 200) : null;
const rangLabel = istGueltigesSprachFeld(body.rangLabel) ? saubereSprachen(body.rangLabel, 40) : null;
const extraCtaLabel = body.extraCta?.href && istGueltigesSprachFeld(body.extraCta?.label) ? saubereSprachen(body.extraCta.label, 60) : null;
const jetzt = nowIso();
const werte = {
id: generateId(),
kategorie, name, slug,
rolle_json: JSON.stringify(rolle),
foto_url: String(body.foto || "").trim().slice(0, 300) || null,
tiktok_url: body.tiktok?.href || (typeof body.tiktok === "string" ? body.tiktok : null),
intro_json: intro ? JSON.stringify(intro) : null,
bio_json: bio ? JSON.stringify(bio) : null,
bio_html_json: bioHtml ? JSON.stringify(bioHtml) : null,
zitat_json: zitat ? JSON.stringify(zitat) : null,
rang: Number.isFinite(Number(body.rang)) ? Number(body.rang) : null,
rang_label_json: rangLabel ? JSON.stringify(rangLabel) : null,
extra_cta_href: extraCtaLabel ? body.extraCta.href : null,
extra_cta_label_json: extraCtaLabel ? JSON.stringify(extraCtaLabel) : null,
reihenfolge: 0,
status: "aktiv",
geaendert_am: jetzt, geaendert_von: session.actor,
};
db.prepare(
`INSERT INTO team_members
(id, kategorie, name, slug, rolle_json, foto_url, tiktok_url, intro_json,
bio_json, bio_html_json, zitat_json, rang, rang_label_json,
extra_cta_href, extra_cta_label_json, reihenfolge, status,
erstellt_am, geaendert_am, erstellt_von, geaendert_von)
VALUES
(@id, @kategorie, @name, @slug, @rolle_json, @foto_url, @tiktok_url, @intro_json,
@bio_json, @bio_html_json, @zitat_json, @rang, @rang_label_json,
@extra_cta_href, @extra_cta_label_json, @reihenfolge, @status,
@geaendert_am, @geaendert_am, @geaendert_von, @geaendert_von)`
).run(werte);
logAction(session.actor, "team.imported", werte.id, { name, kategorie, slug });
const row = db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(werte.id);
return json(res, { ok: true, uebersprungen: false, mitglied: vollesFeld(row) });
}
export async function deleteTeamMember(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const id = String(req.body?.id || "");
const info = db.prepare(`DELETE FROM team_members WHERE id = ?`).run(id);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "team.deleted", id, null);
return json(res, { ok: true });
}
+118
View File
@@ -0,0 +1,118 @@
/* =====================================================================
routes/testimonials.js — "Stimmen" auf stimmen.html (20.08.2026,
Nutzer-Wunsch: "eine richtig geile kachel ... wo die leute mit namen
und tiktok namen mir eine nachricht schreiben können die ich in die
verwaltung kriege, und dann kann ich die ausgewählten über die
verwaltungsseite auf die website hinzufügen").
Öffentlich: jede/jeder darf eine Nachricht einreichen (kein Login
nötig, absichtlich niedrigschwellig -- "mega persönlich zu den Fans").
Landet als Entwurf ("eingereicht"), wird NIE automatisch veröffentlicht.
Nur Dogi/VanVan (TESTIMONIALS_MANAGE) sehen die Warteschlange und
entscheiden pro Eintrag: freigeben (-> erscheint auf stimmen.html),
ablehnen oder löschen. ===================================================================== */
import { db } from "../db.js";
import { generateId, nowIso } from "../lib/crypto.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
function oeffentlichesFeld(row) {
return {
id: row.id,
name: row.name,
tiktokUsername: row.tiktok_username,
nachricht: row.nachricht,
};
}
function vollesFeld(row) {
return {
id: row.id,
name: row.name,
tiktokUsername: row.tiktok_username,
nachricht: row.nachricht,
status: row.status,
createdAt: row.created_at,
entschiedenAm: row.entschieden_am,
entschiedenVon: row.entschieden_von,
};
}
/** Öffentlich, keine Session: eine Nachricht einreichen. Bewusst simpel
gehalten (kein E-Mail-Nachweis, keine Registrierung) -- die einzige
Schutzschicht gegen Missbrauch ist die manuelle Freigabe durch Dogi,
kein Eintrag wird je ungesehen veröffentlicht. */
export async function submitTestimonial(req, res) {
const body = req.body || {};
const name = String(body.name || "").trim().slice(0, 60);
const tiktokUsername = String(body.tiktokUsername || "").trim().replace(/^@/, "").slice(0, 40) || null;
const nachricht = String(body.nachricht || "").trim().slice(0, 500);
if (!name) return json(res, { ok: false, error: "Bitte gib deinen Namen an." }, 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);
const id = generateId();
db.prepare(
`INSERT INTO testimonials (id, name, tiktok_username, nachricht, status, created_at) VALUES (?, ?, ?, ?, 'eingereicht', ?)`
).run(id, name, tiktokUsername, nachricht, nowIso());
logAction("anonym", "testimonial.submitted", id, { name });
return json(res, { ok: true });
}
/** Öffentlich, keine Session: nur freigegebene Stimmen, neueste zuerst. */
export async function getApprovedTestimonialsPublic(req, res) {
const rows = db
.prepare(`SELECT * FROM testimonials WHERE status = 'freigegeben' ORDER BY entschieden_am DESC LIMIT 60`)
.all();
return json(res, { ok: true, testimonials: rows.map(oeffentlichesFeld) });
}
/** Für die Verwaltung: ALLE Einreichungen, neueste zuerst -- offene zuerst
einsortiert, damit die Warteschlange sofort ins Auge fällt. */
export async function getTestimonialsAdmin(req, res, session) {
if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res);
const rows = db
.prepare(
`SELECT * FROM testimonials
ORDER BY CASE status WHEN 'eingereicht' THEN 0 ELSE 1 END, created_at DESC`
)
.all();
return json(res, { ok: true, testimonials: rows.map(vollesFeld) });
}
export async function setTestimonialStatus(req, res, session) {
if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res);
const id = String(req.body?.id || "");
const status = String(req.body?.status || "");
if (!["freigegeben", "abgelehnt", "eingereicht"].includes(status)) {
return json(res, { ok: false, error: "Ungültiger Status." }, 400);
}
const info = db
.prepare(`UPDATE testimonials SET status = ?, entschieden_am = ?, entschieden_von = ? WHERE id = ?`)
.run(status, nowIso(), session.actor, id);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "testimonial.status_changed", id, { status });
return json(res, { ok: true });
}
export async function deleteTestimonial(req, res, session) {
if (!can(session, "TESTIMONIALS_MANAGE")) return forbidden(res);
const id = String(req.body?.id || "");
const info = db.prepare(`DELETE FROM testimonials WHERE id = ?`).run(id);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "testimonial.deleted", id, null);
return json(res, { ok: true });
}
+1
View File
@@ -230,6 +230,7 @@ export function gateMiddleware(req, res, next) {
pfad === "/gate.html" || pfad === "/gate.html" ||
pfad.startsWith("/assets/") || pfad.startsWith("/assets/") ||
pfad === "/manifest.json" || pfad === "/manifest.json" ||
pfad === "/manifest-verwaltung.json" ||
pfad === "/sw.js" || pfad === "/sw.js" ||
pfad === "/favicon.ico" pfad === "/favicon.ico"
) { ) {
+54
View File
@@ -37,6 +37,60 @@ app.use(cookieParser());
app.use(express.json({ limit: "1mb" })); app.use(express.json({ limit: "1mb" }));
app.use(gateMiddleware); app.use(gateMiddleware);
/* Sicherheits-Fix 20.08.2026 (Audit vor dem geplanten öffentlichen Start morgen).
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")) — express.static liefert
ohne diese Sperre daher nicht nur die eigentliche Website aus, sondern auch den Server-
Quellcode (server/, server-internal/, cloudflare-worker/) und das komplette
.git-Verzeichnis samt Commit-Historie.
Live nachgewiesen (mit gültigem Zugangscode, der morgen für alle wegfällt):
/server/gate.js und /.git/config lieferten HTTP 200. Ursache: serve-static blockt per
Voreinstellung nur Dateien, deren EIGENER Name mit einem Punkt beginnt (server/.env ->
404 durch diese Regel) — aber NICHT rekursiv. Eine Datei wie .git/config wird trotzdem
ausgeliefert, weil "config" selbst nicht mit einem Punkt beginnt, nur der Ordnername davor.
Diese Sperre läuft daher bewusst unabhängig von gateMiddleware (bleibt auch nach dem
morgigen Entfernen der Zugangsschranke wirksam) und blockt ganze Ordner/Muster statt
einzelner Dateien, damit neue Dateien darin automatisch mitgeschützt sind. */
const GESPERRTE_ORDNER = ["/server", "/server-internal", "/cloudflare-worker"];
const GESPERRTE_DATEIEN = [
"/CLAUDE.md", "/DEPLOY.md", "/wrangler.toml", "/netlify.toml",
"/gate-worker.js", "/_headers", "/.gitignore", "/.nojekyll", "/.assetsignore",
];
app.use((req, res, next) => {
const pfad = req.path;
const hatVerstecktenOrdner = pfad
.split("/")
.some((teil) => teil.startsWith(".") && teil.length > 1 && teil !== ".well-known");
const inGesperrtemOrdner = GESPERRTE_ORDNER.some((p) => pfad === p || pfad.startsWith(p + "/"));
const istGesperrteDatei = GESPERRTE_DATEIEN.includes(pfad);
const istBackupDatei = /\.bak(-|\.|$)/i.test(pfad);
if (hatVerstecktenOrdner || inGesperrtemOrdner || istGesperrteDatei || istBackupDatei) {
return res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
if (err) res.status(404).send("Nicht gefunden.");
});
}
next();
});
/* Bugfix 20.08.2026 (Nutzer-Report: "ich kann sie immer noch nicht runter
laden und die leiste oben ist immer noch so groß", NACHDEM der jeweilige
Fix schon deployed war): Express selbst schickt standardmäßig GAR KEIN
Cache-Control mit (nur ETag/Last-Modified). Weil dogfather-universe.com
hinter Cloudflare liegt (orange-cloud-Proxy), springt Cloudflare bei
fehlendem Origin-Cache-Control mit seinem EIGENEN Standardwert ein
("Browser Cache TTL", per curl bestätigt: 4 Stunden = max-age=14400) —
ein frischer Deploy war dadurch für Besucher:innen bis zu 4 Std lang im
eigenen Browser-Cache unsichtbar, obwohl der Server längst die neue
Version ausliefert. Cloudflare respektiert laut Doku ein vom Origin
gesetztes Cache-Control statt seinen Standardwert zu benutzen — deshalb
hier explizit "no-cache" (erzwingt bei jedem Laden eine Revalidierung
per ETag/If-None-Match, liefert bei unverändertem Inhalt ein schnelles
304 statt die ganze Datei erneut zu senden — kein "gar nicht cachen",
nur "nie blind auf einen alten Stand vertrauen"). */
app.use((req, res, next) => {
res.setHeader("Cache-Control", "no-cache");
next();
});
app.use(express.static(SITE_DIR, { index: "index.html" })); app.use(express.static(SITE_DIR, { index: "index.html" }));
app.use((req, res) => { app.use((req, res) => {
+48 -17
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -37,27 +37,58 @@
</div> </div>
</section> </section>
<!-- ===== Eigene Stimme einreichen (20.08.2026, Nutzer-Wunsch: "eine
richtig geile kachel ... wo die leute mit namen und tiktok namen
mir eine nachricht schreiben können", "das wird mega persoenlich
zu den fans"). Landet in der Verwaltung, wird NIE automatisch
veroeffentlicht -- siehe server-internal/routes/testimonials.js. -->
<section class="section-tight"> <section class="section-tight">
<div class="container grid grid-3"> <div class="container">
<div class="card"> <div class="stimmen-einreichen-kachel" id="stimmen-formular-box">
<p class="quote" data-i18n="st_quote_community">„Hier steht bald eine echte Stimme aus der Community. 🩵"</p> <span class="stimmen-einreichen-glanz" aria-hidden="true"></span>
<p class="small muted" data-i18n="st_label_community">— Community-Mitglied</p> <div class="stimmen-einreichen-kopf">
<span class="stimmen-einreichen-herz" aria-hidden="true">💌</span>
<h2 data-i18n="st_form_titel">Deine Stimme zählt</h2>
<p class="small" data-i18n="st_form_text">
Was bedeutet dir Team Dogi? Schreib uns deine Geschichte — mit etwas Glück liest DogFather sie
persönlich und stellt sie hier auf die Seite.
</p>
</div> </div>
<div class="card"> <form id="stimmen-formular">
<p class="quote" data-i18n="st_quote_team">„Hier steht bald eine echte Stimme aus dem Team. 🩵"</p> <div class="field">
<p class="small muted" data-i18n="st_label_team">— Team-Mitglied</p> <label for="st-name" data-i18n="st_form_name_label">Dein Name</label>
<input type="text" id="st-name" maxlength="60" required />
</div> </div>
<div class="card"> <div class="field">
<p class="quote" data-i18n="st_quote_scout">„Hier steht bald eine echte Stimme von Scouts &amp; Creatorn. 🩵"</p> <label for="st-tiktok" data-i18n="st_form_tiktok_label">TikTok-Name (optional)</label>
<p class="small muted" data-i18n="st_label_scout">— Scout/Creator</p> <input type="text" id="st-tiktok" maxlength="40" placeholder="@dein-name" />
</div>
<div class="field">
<label for="st-nachricht" data-i18n="st_form_nachricht_label">Deine Nachricht</label>
<textarea id="st-nachricht" maxlength="500" rows="4" required data-i18n-attr="placeholder:st_form_nachricht_placeholder" placeholder="Erzähl uns, was dir Team Dogi bedeutet …"></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;" data-i18n="st_form_absenden">💙 Nachricht einreichen</button>
</form>
<p class="stimmen-erfolg" id="stimmen-erfolg" data-i18n="st_form_erfolg">Danke dir! Deine Nachricht wurde eingereicht und wird von Dogi persönlich geprüft. 🩵</p>
<p class="stimmen-fehler" id="stimmen-fehler"></p>
</div> </div>
</div> </div>
<!-- TODO: Echte Testimonials von Community, Team, Scouts und Creator sammeln (mit Einverständnis zur Veröffentlichung). Bis dahin bleiben hier Platzhalter stehen. --> </section>
<!-- ===== Echte, freigegebene Stimmen -- bleibt komplett unsichtbar, bis
mindestens eine Nachricht in der Verwaltung freigegeben wurde (kein
Platzhaltertext mehr). ===== -->
<section class="section-tight" id="stimmen-echte-root" hidden>
<div class="container">
<h2 class="text-center" data-i18n="st_echte_titel">Das sagt die Community</h2>
<div class="grid grid-3" id="stimmen-echte-grid" style="margin-top:1.6rem;"></div>
</div>
</section> </section>
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-stimmen.js"></script> <script src="assets/js/i18n-stimmen.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script src="assets/js/stimmen.js?v=20260821n"></script>
</body> </body>
</html> </html>
+37 -9
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -42,10 +42,21 @@
</section> </section>
<section class="section-tight"> <section class="section-tight">
<div class="container" style="max-width:70ch;"> <!-- Echtes Foto von Filipe (Nutzer-Wunsch 21.08.2026: "füge das foto ... neben den
text im screen1"), assets/img/streamer-filipe-portrait.jpg -- aus
C:\Users\qciga\Pictures\Filipe\..., auf 1000x1500 (Original-Seitenverhältnis
2:3) verkleinert, damit object-fit:cover das Bild komplett unbeschnitten zeigt.
frame-gold statt frame-lila (siehe Casper-Sektion weiter unten) für optische
Abwechslung zwischen den Bild+Text-Abschnitten der Seite. -->
<div class="container grid grid-2" style="align-items:center;gap:2.6rem;">
<div>
<p data-i18n="str_intro1">Ich bin 34 Jahre alt, in Luxemburg geboren und aufgewachsen — mit portugiesischen Wurzeln und dem Temperament, das mich ausmacht. Mein bisheriger Weg führte mich durch verschiedene Orte, unterschiedliche Lebensabschnitte und Erfahrungen, die mich zu dem Menschen gemacht haben, der ich heute bin.</p> <p data-i18n="str_intro1">Ich bin 34 Jahre alt, in Luxemburg geboren und aufgewachsen — mit portugiesischen Wurzeln und dem Temperament, das mich ausmacht. Mein bisheriger Weg führte mich durch verschiedene Orte, unterschiedliche Lebensabschnitte und Erfahrungen, die mich zu dem Menschen gemacht haben, der ich heute bin.</p>
<p class="quote" data-i18n="str_intro_quote" style="margin-top:1.4rem;">Doch DogFather ist nicht einfach nur ein Name im Internet. DogFather ist ein Teil von mir.</p> <p class="quote" data-i18n="str_intro_quote" style="margin-top:1.4rem;">Doch DogFather ist nicht einfach nur ein Name im Internet. DogFather ist ein Teil von mir.</p>
</div> </div>
<div class="collage-photo frame-gold" style="aspect-ratio:2/3;border-radius:var(--radius);">
<img src="assets/img/streamer-filipe-portrait.jpg" alt="Filipe alias DogFather" style="width:100%;height:100%;object-fit:cover;object-position:center top;" />
</div>
</div>
</section> </section>
<section> <section>
@@ -73,11 +84,28 @@
<section> <section>
<div class="container grid grid-2" style="align-items:center;"> <div class="container grid grid-2" style="align-items:center;">
<div class="collage-photo frame-lila" style="aspect-ratio:auto;border-radius:var(--radius);"> <!-- Bugfix 20.08.2026 (Nutzer-Report: "der soll bissl kleiner sein und nicht
<img src="assets/img/streamer-mascot.jpg" alt="DogFather Maskottchen" style="width:100%;height:auto;display:block;" /> so riesig, einfach normal wie auf der startseite"): streamer-mascot.jpg ist
900x2812 px, also extrem hochkant. Mit aspect-ratio:auto + height:auto lief
es in voller natürlicher Höhe und war damit gut dreimal so hoch wie die
Textspalte daneben.
Update 21.08.2026 (Nutzer-Report: "die größe vom bild vom husky soll dem
text rechts nebendran angepasst werden"): der erste Deckel (460px) war zu
knapp und ließ eine sichtbare Lücke unter dem Bild. Ein Grid-Stretch-Ansatz
(align-items:stretch + height:100%) wurde ausprobiert, zieht bei einem so
extremen Hochkant-Seitenverhältnis aber die ganze Zeile auf die riesige
Eigenhöhe des Bildes hoch (Grid richtet 'auto'-Zeilen am Max-Content-Beitrag
aus) -- daher verworfen. Stattdessen ein großzügigerer fester Deckel
(siehe .collage-photo-kompakt in main.css, jetzt 720px statt 460px), der
sich an der typischen Höhe der Textspalte in diesem Abschnitt orientiert. -->
<div class="collage-photo collage-photo-kompakt frame-lila" style="border-radius:var(--radius);">
<img src="assets/img/streamer-mascot.jpg" alt="DogFather Maskottchen" />
</div> </div>
<div> <div>
<h2 data-i18n="str_casper_h2">🐺 Casper – mein treuer Begleiter</h2> <h2 data-i18n="str_casper_h2">🐺 Casper – mein treuer Begleiter</h2>
<!-- TikTok-Account der Modi-Gang: 21.08.2026 auf Nutzer-Wunsch nach team-modis.html
umgezogen ("den knopf da weg bitte und in die modie seite zu den modis
hinzufügen") -- passt thematisch besser dort hin, siehe dort. -->
<p data-i18n="str_casper_1">Ein ganz besonderer Teil meines Lebens und auch meiner Community ist mein Husky <a href="casper.html"><strong>Casper</strong></a>.</p> <p data-i18n="str_casper_1">Ein ganz besonderer Teil meines Lebens und auch meiner Community ist mein Husky <a href="casper.html"><strong>Casper</strong></a>.</p>
<p data-i18n="str_casper_2">Casper wurde adoptiert und stammt aus einem Kriegsgebiet in der Ukraine. Trotz allem, was er erlebt hat, besitzt er ein unglaublich großes und liebevolles Herz.</p> <p data-i18n="str_casper_2">Casper wurde adoptiert und stammt aus einem Kriegsgebiet in der Ukraine. Trotz allem, was er erlebt hat, besitzt er ein unglaublich großes und liebevolles Herz.</p>
<p data-i18n="str_casper_3">In vielen Dingen ist Casper genauso wie sein Papa: ruhig, lieb, manchmal völlig verpeilt und immer für Dummheiten zu haben. Doch sobald es um Menschen oder Dinge geht, die er liebt, zeigt er seine beschützende Seite.</p> <p data-i18n="str_casper_3">In vielen Dingen ist Casper genauso wie sein Papa: ruhig, lieb, manchmal völlig verpeilt und immer für Dummheiten zu haben. Doch sobald es um Menschen oder Dinge geht, die er liebt, zeigt er seine beschützende Seite.</p>
@@ -189,7 +217,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-streamer.js"></script> <script src="assets/js/i18n-streamer.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+75 -8
View File
@@ -19,21 +19,87 @@
<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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamplan.jpg');--page-bg-mobile:url('/assets/img/bg-streamplan-mobile.jpg');"> <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');">
<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" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821n" 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>
<p class="lead worlds" data-i18n="sp_lead">Wann ist DogFather live?</p> <p class="lead worlds" data-i18n="sp_lead">Wann ist DogFather live?</p>
<!-- Live-Radar (20.08.2026, Nutzer-Wunsch "geiler und spezieller"): 24h-Ziffernblatt
mit festem 20-Uhr-Leuchtpunkt + echtem "Jetzt"-Zeiger, schaltet live auf Rot/Puls
um, sonst zählt es zum nächsten 20-Uhr-Termin runter. Zweite Uhr (rechts, Magenta/
Violett) für ein optionales "spezielles Live-Event" aus der Verwaltung -- nur
sichtbar, wenn dort eins aktiviert ist. Siehe assets/js/streamplan.js. -->
<div class="stream-radar-reihe">
<div class="stream-radar-wrap">
<div class="stream-radar" id="stream-radar">
<div class="stream-radar-sweep" aria-hidden="true"></div>
<svg viewBox="0 0 300 300" class="stream-radar-dial" aria-hidden="true">
<defs>
<linearGradient id="radarGradTaeglich" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#8fd9ea"></stop>
<stop offset="50%" stop-color="#8b5cf6"></stop>
<stop offset="100%" stop-color="#e8c465"></stop>
</linearGradient>
</defs>
<circle cx="150" cy="150" r="130" class="radar-ring-outer" stroke="url(#radarGradTaeglich)"></circle>
<circle cx="150" cy="150" r="94" class="radar-ring-inner"></circle>
<g id="radar-ticks"></g>
<g id="radar-fixmarker"></g>
<line id="radar-now-hand" class="radar-now-hand" x1="150" y1="150" x2="150" y2="50"></line>
<circle id="radar-now-dot" class="radar-now-dot" cx="150" cy="50" r="5"></circle>
<circle cx="150" cy="150" r="4" class="radar-center-dot"></circle>
</svg>
<div class="stream-radar-center">
<span class="stream-radar-dot" id="radar-status-dot"></span>
<span class="stream-radar-status" id="radar-status-text" data-i18n="sp_radar_loading">Lädt …</span>
<span class="stream-radar-countdown" id="radar-countdown"></span>
<span class="stream-radar-sub" id="radar-status-sub"></span>
</div>
</div>
<a class="btn btn-primary stream-radar-watch-btn" id="radar-watch-btn" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener" hidden data-i18n="sp_radar_watch_btn">🔴 Jetzt anschauen</a>
<p class="small muted stream-radar-hinweis" data-i18n="sp_radar_hinweis">Alle Zeiten in deiner Ortszeit.</p>
</div>
<div class="stream-radar-wrap" id="se-radar-wrap" hidden>
<div class="stream-radar event-spezial" id="se-radar">
<div class="stream-radar-sweep" aria-hidden="true"></div>
<svg viewBox="0 0 300 300" class="stream-radar-dial" aria-hidden="true">
<defs>
<linearGradient id="radarGradSpezial" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff5cb8"></stop>
<stop offset="50%" stop-color="#c9a6ff"></stop>
<stop offset="100%" stop-color="#e8c465"></stop>
</linearGradient>
</defs>
<circle cx="150" cy="150" r="130" class="radar-ring-outer" stroke="url(#radarGradSpezial)"></circle>
<circle cx="150" cy="150" r="94" class="radar-ring-inner"></circle>
<g id="se-radar-ticks"></g>
<g id="se-radar-fixmarker"></g>
<line id="se-radar-now-hand" class="radar-now-hand" x1="150" y1="150" x2="150" y2="50"></line>
<circle id="se-radar-now-dot" class="radar-now-dot" cx="150" cy="50" r="5"></circle>
<circle cx="150" cy="150" r="4" class="radar-center-dot"></circle>
</svg>
<div class="stream-radar-center">
<span class="stream-radar-titel" id="se-radar-titel"></span>
<span class="stream-radar-countdown" id="se-radar-countdown"></span>
<span class="stream-radar-sub" data-i18n="sp_radar_hinweis_spezial">Spezielles Live-Event</span>
</div>
</div>
<a class="btn btn-primary stream-radar-watch-btn" id="se-radar-link" href="#" target="_blank" rel="noopener" hidden data-i18n="sp_radar_mehr_erfahren">Mehr erfahren →</a>
</div>
</div>
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
<a class="btn-tiktok" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener"><span class="btn-tiktok-note" aria-hidden="true">♪</span><span class="btn-tiktok-text" data-i18n="sp_btn_tiktok">Zur Dogi TikTok-Seite</span></a> <a class="btn-tiktok" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener"><span class="btn-tiktok-note" aria-hidden="true">♪</span><span class="btn-tiktok-text" data-i18n="sp_btn_tiktok">Zur Dogi TikTok-Seite</span></a>
</div> </div>
@@ -60,7 +126,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-streamplan.js"></script> <script src="assets/js/i18n-streamplan.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script src="assets/js/streamplan.js?v=20260821n"></script>
</body> </body>
</html> </html>
+135 -7
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<style> <style>
/* ===================================================================== /* =====================================================================
Supporter-Bereich — komplett neu gestaltet (Nutzer-Feedback Supporter-Bereich — komplett neu gestaltet (Nutzer-Feedback
@@ -42,6 +42,22 @@
.abo-erfolg { color: #baf7d6; font-size: .88rem; margin-top: .5rem; display: none; } .abo-erfolg { color: #baf7d6; font-size: .88rem; margin-top: .5rem; display: none; }
.abo-erfolg.zeige { display: block; } .abo-erfolg.zeige { display: block; }
/* Supporter-Abstimmungen (20.08.2026) — im selben Gold-Ton wie der Rest
des Supporter-Bereichs, keine eigene Farbwelt. */
.poll-option-btn {
display: block; width: 100%; text-align: left; padding: .8rem 1rem; margin-bottom: .6rem;
border-radius: 10px; border: 1.5px solid var(--border); background: rgba(255,255,255,.03);
color: var(--text); font: inherit; cursor: pointer; transition: border-color .15s ease, background .15s ease;
}
.poll-option-btn:hover { border-color: var(--accent-2); }
.poll-option-btn.gewaehlt { border-color: var(--accent-2); background: rgba(212,175,55,.14); font-weight: 700; }
.poll-result-row { margin-bottom: .8rem; }
.poll-result-row:last-child { margin-bottom: 0; }
.poll-result-head { display: flex; justify-content: space-between; gap: 1rem; font-size: .88rem; margin-bottom: .3rem; }
.poll-result-head.meine-stimme { color: #ffe9a8; font-weight: 800; }
.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; }
/* 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. */
@@ -323,6 +339,18 @@
<div class="container"> <div class="container">
<div id="supp-kuendigungs-hinweis" class="supp-hinweis" style="display:none;"></div> <div id="supp-kuendigungs-hinweis" class="supp-hinweis" style="display:none;"></div>
<!-- Supporter-Abstimmungen (20.08.2026) — nur sichtbar, wenn gerade
eine Abstimmung aktiv ist bzw. noch nicht abgestimmt wurde, siehe
ladePoll() weiter unten. -->
<div class="card supp-card" id="supp-poll-card" style="display:none;margin-bottom:2.4rem;">
<span class="eyebrow" data-i18n="su_poll_eyebrow">🗳️ Aktuelle Abstimmung</span>
<h3 id="poll-frage-text" style="margin:.5rem 0 1.1rem;">–</h3>
<div id="poll-optionen-container"></div>
<button type="button" class="btn btn-primary" id="poll-abstimmen-btn" style="margin-top:.4rem;" data-i18n="su_poll_btn_abstimmen">Abstimmen</button>
<p class="abo-erfolg" id="poll-erfolg" data-i18n="su_poll_danke">Danke, deine Stimme wurde gezählt! 💙</p>
<p class="abo-fehler" id="poll-fehler"></p>
</div>
<h2 data-i18n="su_fenster1_h2">Mein Abo</h2> <h2 data-i18n="su_fenster1_h2">Mein Abo</h2>
<div class="card supp-card"> <div class="card supp-card">
<div class="supp-stats"> <div class="supp-stats">
@@ -388,9 +416,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-supporter.js"></script> <script src="assets/js/i18n-supporter.js?v=20260821n"></script>
<script src="assets/js/supporter.js"></script> <script src="assets/js/supporter.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
(function () { (function () {
const S = window.DogiSupporter; const S = window.DogiSupporter;
@@ -600,8 +628,108 @@
// Profil-Formular vorbefüllen // Profil-Formular vorbefüllen
document.getElementById("edit-name").value = data.profile.displayName || ""; document.getElementById("edit-name").value = data.profile.displayName || "";
document.getElementById("edit-tiktok").value = data.profile.tiktokUsername || ""; document.getElementById("edit-tiktok").value = data.profile.tiktokUsername || "";
// Supporter-Abstimmungen (20.08.2026) — nur mit echtem Token abrufen,
// nicht im Vorschau-Modus mit Beispieldaten (kein API-Aufruf gewollt).
if (!nurMockNoetig) await ladePoll();
else document.getElementById("supp-poll-card").style.display = "none";
} }
/* ---------- Supporter-Abstimmungen (20.08.2026) ----------
Zeigt die aktuell aktive Abstimmung (falls vorhanden). Vor der eigenen
Stimme: nur die Antwortoptionen zum Anklicken, keine Ergebnisse (damit
sie die eigene Wahl nicht beeinflussen). Nach der Stimme (auch bei
einem erneuten Seitenaufruf, dank myVote vom Server): Ergebnisbalken
statt Auswahl-Buttons. */
const feldSprache = (mehrsprachigesFeld) =>
window.dogiFeld ? window.dogiFeld(mehrsprachigesFeld) : mehrsprachigesFeld?.de || "";
let aktuellePoll = null;
let gewaehlteOption = null;
function renderPollErgebnisse(poll, counts, totalVotes, meineOption) {
const container = document.getElementById("poll-optionen-container");
container.innerHTML = poll.options
.map((opt, i) => {
const n = counts?.[i] || 0;
const proz = totalVotes ? Math.round((n / totalVotes) * 100) : 0;
const istMeine = i === meineOption;
return `<div class="poll-result-row">
<div class="poll-result-head${istMeine ? " meine-stimme" : ""}">
<span>${istMeine ? "✓ " : ""}${escapeHtmlPoll(feldSprache(opt))}</span>
<span>${n} ${escapeHtmlPoll(feldSprache(window.I18N_PAGE?.su_poll_stimmen))} · ${proz}%</span>
</div>
<div class="poll-bar-track"><div class="poll-bar-fill" style="width:${proz}%;"></div></div>
</div>`;
})
.join("");
document.getElementById("poll-abstimmen-btn").style.display = "none";
}
function escapeHtmlPoll(text) {
return String(text ?? "").replace(/[&<>"']/g, (z) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[z]));
}
function renderPollAuswahl(poll) {
const container = document.getElementById("poll-optionen-container");
gewaehlteOption = null;
container.innerHTML = poll.options
.map((opt, i) => `<button type="button" class="poll-option-btn" data-option-index="${i}">${escapeHtmlPoll(feldSprache(opt))}</button>`)
.join("");
container.querySelectorAll(".poll-option-btn").forEach((btn) => {
btn.addEventListener("click", () => {
gewaehlteOption = Number(btn.dataset.optionIndex);
container.querySelectorAll(".poll-option-btn").forEach((b) => b.classList.toggle("gewaehlt", b === btn));
});
});
document.getElementById("poll-abstimmen-btn").style.display = "";
}
async function ladePoll() {
const karte = document.getElementById("supp-poll-card");
let res;
try {
res = await S.apiFetch("/supporter/polls/active", {}, true);
} catch {
karte.style.display = "none";
return;
}
if (!res.poll) {
karte.style.display = "none";
return;
}
aktuellePoll = res.poll;
karte.style.display = "";
document.getElementById("poll-frage-text").textContent = feldSprache(res.poll.question);
document.getElementById("poll-erfolg").classList.remove("zeige");
document.getElementById("poll-fehler").classList.remove("zeige");
if (res.myVote !== null && res.myVote !== undefined) {
renderPollErgebnisse(res.poll, res.counts, res.totalVotes, res.myVote);
document.getElementById("poll-erfolg").classList.add("zeige");
} else {
renderPollAuswahl(res.poll);
}
}
document.getElementById("poll-abstimmen-btn").addEventListener("click", async () => {
const fehlerEl = document.getElementById("poll-fehler");
fehlerEl.classList.remove("zeige");
if (gewaehlteOption === null || !aktuellePoll) {
fehlerEl.textContent = feldSprache(window.I18N_PAGE?.su_poll_bitte_waehlen) || "Bitte wähle eine Antwort aus.";
fehlerEl.classList.add("zeige");
return;
}
try {
const res = await S.apiFetch("/supporter/polls/vote", { pollId: aktuellePoll.id, optionIndex: gewaehlteOption }, true);
renderPollErgebnisse(aktuellePoll, res.counts, res.totalVotes, gewaehlteOption);
document.getElementById("poll-erfolg").classList.add("zeige");
} catch (err) {
fehlerEl.textContent = err.message || "Abstimmen hat leider nicht geklappt. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
}
});
/* ---------- Detail-Modals für die klickbaren Kennzahl-Kärtchen ---------- /* ---------- Detail-Modals für die klickbaren Kennzahl-Kärtchen ----------
Nutzer-Wunsch 04.08.2026: "wenn man auf diese 2 Kisten drückt, sollen Nutzer-Wunsch 04.08.2026: "wenn man auf diese 2 Kisten drückt, sollen
die viel mehr Infos kriegen ... richtig geil". Recherche, was für die viel mehr Infos kriegen ... richtig geil". Recherche, was für
+38 -12
View File
@@ -19,10 +19,10 @@
<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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -32,8 +32,14 @@
<div class="hero-banner"><img src="/assets/img/bg-dogfather.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-dogfather.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">Das Modi-Team</h1> <h1 data-i18n="tm_h1" data-site-text-key="seitentitel_modi">Das Modi-Team</h1>
<p class="lead worlds" data-i18n="tm_lead">Das Rückgrat von Team Dogi — von VanVan bis Marina.</p> <p class="lead worlds" data-i18n="tm_lead">Das Rückgrat von Team Dogi — von VanVan bis Marina.</p>
<!-- TikTok-Account der Modi-Gang (Nutzer-Wunsch 20.08.2026, umgezogen 21.08.2026 von
streamer.html hierher: "in die modie seite zu den modis hinzufügen"). Passt hier
thematisch besser hin, weil der Account das ganze Modi-Team repräsentiert. -->
<div class="btn-row" style="justify-content:center;">
<a class="btn-tiktok" href="https://www.tiktok.com/@dogis.modi.gang" target="_blank" rel="noopener"><span class="btn-tiktok-note" aria-hidden="true">♪</span><span class="btn-tiktok-text" data-i18n="tm_btn_tiktok_modigang">Zur Modi-Gang TikTok-Seite</span></a>
</div>
</div> </div>
</section> </section>
@@ -66,24 +72,31 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script> <script src="assets/js/data-modis.js?v=20260821n"></script>
<script src="assets/js/i18n-team-modis.js"></script> <script src="assets/js/i18n-team-modis.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("modis-grid"); const grid = document.getElementById("modis-grid");
if (!grid || !window.MODIS) return; if (!grid || !window.MODIS) return;
const rest = window.MODIS.filter((m) => m.name !== "VanVan").sort((a, b) => a.rank - b.rank);
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || ""); const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel); const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel);
const MAX_STUFEN = 5; // fünf definierte Größenstufen im CSS
// 21.08.2026: rest/raenge werden jetzt INNERHALB von renderModis() frisch aus
// window.MODIS berechnet (statt einmalig beim Setup) -- nötig, damit über die
// Verwaltung nachträglich angehängte Modis (siehe dogiTeamLaden weiter unten)
// beim erneuten Aufruf von renderModis() tatsächlich mit auftauchen. Ein
// admin-gepflegter Modi ohne eigenen Rang fällt dabei (?? 999) ganz ans Ende
// der Pyramide, statt die Sortierung der bestehenden Ränge durcheinander zu
// bringen.
const renderModis = () => {
const rest = window.MODIS.filter((m) => m.name !== "VanVan").sort((a, b) => (a.rank ?? 999) - (b.rank ?? 999));
// Pyramide: eine Ebene pro eindeutigem Rang, oben schmal/groß, nach // Pyramide: eine Ebene pro eindeutigem Rang, oben schmal/groß, nach
// unten breiter/kompakter — genau wie ein Organigramm. // unten breiter/kompakter — genau wie ein Organigramm.
const raenge = [...new Set(rest.map((m) => m.rank))]; const raenge = [...new Set(rest.map((m) => m.rank))];
const MAX_STUFEN = 5; // fünf definierte Größenstufen im CSS
const renderModis = () => {
grid.innerHTML = raenge grid.innerHTML = raenge
.map((rang, i) => { .map((rang, i) => {
const stufe = Math.min(i + 1, MAX_STUFEN); const stufe = Math.min(i + 1, MAX_STUFEN);
@@ -119,6 +132,19 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
renderModis(); renderModis();
document.addEventListener("dogi-sprache-geaendert", renderModis); document.addEventListener("dogi-sprache-geaendert", renderModis);
// 21.08.2026: über die Verwaltung angelegte/importierte Modis einmischen
// (+ Runde 2: "wenn ich eine änderung mache ist es auch für jeden").
// Rang/Rang-Bezeichnung kommen mit, damit sie sich sauber in die
// bestehende Pyramide einordnen. dogiTeamZusammenfuehren() überschreibt
// bereits importierte Modis (gleicher slug) mit ihrem aktuellen DB-Stand.
if (window.dogiTeamLaden) {
window.dogiTeamLaden("modi").then((neue) => {
if (!neue.length) return;
window.MODIS = window.dogiTeamZusammenfuehren(window.MODIS, neue);
renderModis();
});
}
}); });
</script> </script>
</body> </body>
+22 -8
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-scouts.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-scouts.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="ts_eyebrow">Manager DogFather — Spicy Media</span> <span class="eyebrow" data-i18n="ts_eyebrow">Manager DogFather — Spicy Media</span>
<h1 data-i18n="ts_h1">Mein Scout-Team</h1> <h1 data-i18n="ts_h1" data-site-text-key="seitentitel_scout">Mein Scout-Team</h1>
<p class="lead worlds" data-i18n="ts_lead">Sie finden und begleiten neue Talente für Dogi.</p> <p class="lead worlds" data-i18n="ts_lead">Sie finden und begleiten neue Talente für Dogi.</p>
</div> </div>
</section> </section>
@@ -49,9 +49,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-scouts.js"></script> <script src="assets/js/data-scouts.js?v=20260821n"></script>
<script src="assets/js/i18n-team-scouts.js"></script> <script src="assets/js/i18n-team-scouts.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("scouts-grid"); const grid = document.getElementById("scouts-grid");
@@ -76,6 +76,20 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
renderScouts(); renderScouts();
document.addEventListener("dogi-sprache-geaendert", renderScouts); document.addEventListener("dogi-sprache-geaendert", renderScouts);
// 21.08.2026: über die Verwaltung angelegte/importierte Scouts einmischen
// (Nutzer-Wunsch "überall wo ... Scouts ... sind, will ich dass ich die
// easy über meine Verwaltungsseite hinzufüge" + Runde 2: "wenn ich eine
// änderung mache ist es auch für jeden"). dogiTeamZusammenfuehren()
// überschreibt dabei bereits importierte Scouts (gleicher slug) mit ihrem
// aktuellen DB-Stand statt sie zu duplizieren.
if (window.dogiTeamLaden) {
window.dogiTeamLaden("scout").then((neue) => {
if (!neue.length) return;
window.SCOUTS = window.dogiTeamZusammenfuehren(window.SCOUTS, neue);
renderScouts();
});
}
}); });
</script> </script>
</body> </body>
+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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<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"></script> <script src="assets/js/i18n-tt-muddi.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
<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"></script> <script src="assets/js/i18n-vanvan.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+1178 -6
View File
File diff suppressed because it is too large Load Diff
+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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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"></script> <script src="assets/js/i18n-werte.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></script>
</body> </body>
</html> </html>
+70 -71
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" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821n" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821n" />
</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>
@@ -39,25 +39,45 @@
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
<!-- Nutzer-Wunsch 20.08.2026: gleich am Anfang gut sichtbar zeigen, dass
diese Zeitreise noch aktiv wächst/nicht fertig ist — bewusst NICHT
die alte .todo-note-Optik (die war für Entwickler gedacht und wurde
am 31.07.2026 sitewide unsichtbar gemacht), sondern ein einladender,
für Besucher gestalteter Hinweis im Wachstums-Baum-Thema dieser
Seite (passt zu 🌱 "Der Anfang" oben und 🌳 "...wächst weiter" unten). -->
<div class="zeit-baustelle">
<span class="zeit-baustelle-icon" aria-hidden="true">🌱</span>
<div>
<strong data-i18n="zr_baustelle_h3">Diese Zeitreise wächst noch</strong>
<p data-i18n="zr_baustelle_p">Wir ergänzen laufend weitere Kapitel, Daten und Momente aus der Geschichte von DogFather &amp; Team Dogi — schau gern öfter mal wieder vorbei.</p>
</div>
</div>
<div class="zeit-baum"> <div class="zeit-baum">
<div class="zeit-baum-kappe"> <div class="zeit-baum-kappe">
<span aria-hidden="true">🌱</span> <span aria-hidden="true">🌱</span>
<span data-i18n="zr_baum_start">Der Anfang</span> <span data-i18n="zr_baum_start">Der Anfang</span>
</div> </div>
<!-- Nutzer-Wunsch 20.08.2026: echtes Datum nachgetragen (24. Oktober 2023) --
bleibt weiterhin die erste Karte, da es das früheste Datum der ganzen
Zeitleiste ist. -->
<div class="zeit-ast"> <div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div> <div class="zeit-datum" data-i18n="zr_ev1_date">24. Oktober 2023</div>
<h3 data-i18n="zr_ev1_h3">Start als Content Creator</h3> <h3 data-i18n="zr_ev1_h3">Start als Content Creator</h3>
<p data-i18n="zr_ev1_p">DogFather beginnt seine Reise auf TikTok.</p> <p data-i18n="zr_ev1_p">DogFather beginnt seine Reise auf TikTok.</p>
</div> </div>
</div> </div>
<!-- Nutzer-Wunsch 20.08.2026: echtes Datum nachgetragen (Anfang 2025) — bleibt
an dieser Stelle, da "Anfang 2025" chronologisch weiterhin vor "Ostern
2025" (nächste Karte) liegt. -->
<div class="zeit-ast"> <div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div> <div class="zeit-datum" data-i18n="zr_ev2_date">Anfang 2025</div>
<h3 data-i18n="zr_ev2_h3">Team Dogi entsteht</h3> <h3 data-i18n="zr_ev2_h3">Team Dogi entsteht</h3>
<p data-i18n="zr_ev2_p">Aus einzelnen Zuschauern wird eine starke Gemeinschaft.</p> <p data-i18n="zr_ev2_p">Aus einzelnen Zuschauern wird eine starke Gemeinschaft.</p>
</div> </div>
@@ -72,6 +92,30 @@
</div> </div>
</div> </div>
<!-- Nutzer-Wunsch 20.08.2026: echtes Datum nachgetragen (November 2025) und
an die chronologisch richtige Stelle verschoben (lag vorher weiter unten
unter "Datum folgt"). -->
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_ev13_date">November 2025</div>
<h3 data-i18n="zr_ev13_h3">Das Buch „HasiDog &amp; Casper"</h3>
<p data-i18n="zr_ev13_p">Die Geschichte von DogFather, HasiDog und seinem Husky Casper wird zum Buch.</p>
</div>
</div>
<!-- Nutzer-Wunsch 20.08.2026: echtes Datum nachgetragen (1. Januar 2026) und
an die chronologisch richtige Stelle verschoben (lag vorher weiter unten
unter "Datum folgt"). -->
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_ev14_date">1. Januar 2026</div>
<h3 data-i18n="zr_ev14_h3">HasiDog startet auf Spotify</h3>
<p data-i18n="zr_ev14_p">HasiDog erhält sein eigenes Musikprojekt.</p>
</div>
</div>
<div class="zeit-ast meilenstein"> <div class="zeit-ast meilenstein">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
@@ -81,15 +125,6 @@
</div> </div>
</div> </div>
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_ev5_date">Mai 2026</div>
<h3 data-i18n="zr_ev5_h3">Ein neues Kapitel beginnt</h3>
<p data-i18n="zr_ev5_p">DogFather beendet die Zusammenarbeit mit seiner bisherigen Agentur.</p>
</div>
</div>
<div class="zeit-ast"> <div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
@@ -108,12 +143,17 @@
</div> </div>
</div> </div>
<div class="zeit-ast"> <!-- Nutzer-Wunsch 21.08.2026: neue Station, chronologisch nach "Juli 2026 —
Start als Manager" (kein genauer Tag) und vor "27. Juli bis 2. August 2026 —
Creator Cup" einsortiert, weil 25. Juli das genauere Datum ist. Als
Meilenstein markiert -- zwei besondere Premieren an einem Tag (erstes
TiliDog-Fan-Treffen UND erstes persönliches Treffen von DogFather und Tili). -->
<div class="zeit-ast meilenstein">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_ev8_date">Juli 2026</div> <div class="zeit-datum" data-i18n="zr_ev21_date">25. Juli 2026</div>
<h3 data-i18n="zr_ev8_h3">Das Manager-Team wächst</h3> <h3 data-i18n="zr_ev21_h3">Erstes Fan-Treffen: Team TiliDog</h3>
<p data-i18n="zr_ev8_p">Mit VanVan und Diene entsteht eine klare Teamstruktur.</p> <p data-i18n="zr_ev21_p">In Wuppertal kommt die Community von Dogi &amp; Tili zum allerersten Mal persönlich zusammen – ein richtig mega Tag voller guter Laune. Es ist gleichzeitig etwas ganz Besonderes: das erste Aufeinandertreffen von DogFather und Tili überhaupt.</p>
</div> </div>
</div> </div>
@@ -122,7 +162,7 @@
<div class="zeit-karte"> <div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_ev9_date">27. Juli bis 2. August 2026</div> <div class="zeit-datum" data-i18n="zr_ev9_date">27. Juli bis 2. August 2026</div>
<h3 data-i18n="zr_ev9_h3">Der Creator Cup</h3> <h3 data-i18n="zr_ev9_h3">Der Creator Cup</h3>
<p data-i18n="zr_ev9_p">Gemeinsam mit Cidem organisiert DogFather den Creator Cup.</p> <p data-i18n="zr_ev9_p">Gemeinsam mit Cidgem organisiert DogFather den Creator Cup.</p>
</div> </div>
</div> </div>
@@ -153,21 +193,16 @@
</div> </div>
</div> </div>
<div class="zeit-ast"> <!-- Reihenfolge getauscht (Nutzer-Wunsch 20.08.2026): der Website-Start hat
ein konkretes Datum (21.08.2026) und steht deshalb VOR der Teddy-
Kooperation, deren Datum noch offen ist ("Datum folgt") -- so bleibt die
Zeitleiste durchgehend chronologisch lesbar. -->
<div class="zeit-ast meilenstein">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div> <div class="zeit-datum" data-i18n="zr_ev19_date">21. August 2026, 21 Uhr</div>
<h3 data-i18n="zr_ev13_h3">Das Buch „HasiDog &amp; Casper"</h3> <h3 data-i18n="zr_ev19_h3">DOGFATHER UNIVERSE geht online</h3>
<p data-i18n="zr_ev13_p">Die Geschichte von DogFather, HasiDog und seinem Husky Casper wird zum Buch.</p> <p data-i18n="zr_ev19_p">Die offizielle Website öffnet ihre Türen.</p>
</div>
</div>
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div>
<h3 data-i18n="zr_ev14_h3">HasiDog startet auf Spotify</h3>
<p data-i18n="zr_ev14_p">HasiDog erhält sein eigenes Musikprojekt.</p>
</div> </div>
</div> </div>
@@ -180,42 +215,6 @@
</div> </div>
</div> </div>
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div>
<h3 data-i18n="zr_ev16_h3">Die ersten betreuten Creator</h3>
<p data-i18n="zr_ev16_p">Die ersten Creator schließen sich dem Team von Manager DogFather an.</p>
</div>
</div>
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div>
<h3 data-i18n="zr_ev17_h3">Weitere Scouts und Manager kommen hinzu</h3>
<p data-i18n="zr_ev17_p">Das Team soll langfristig weiter wachsen.</p>
</div>
</div>
<div class="zeit-ast">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_datum_folgt">Datum folgt</div>
<h3 data-i18n="zr_ev18_h3">Das erste Community-Treffen</h3>
<p data-i18n="zr_ev18_p">Team Dogi begegnet sich nicht mehr nur im Livestream oder auf Discord.</p>
</div>
</div>
<div class="zeit-ast meilenstein">
<div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte">
<div class="zeit-datum" data-i18n="zr_ev19_date">2026 – Datum folgt</div>
<h3 data-i18n="zr_ev19_h3">DOGFATHER UNIVERSE geht online</h3>
<p data-i18n="zr_ev19_p">Die offizielle Website öffnet ihre Türen.</p>
</div>
</div>
<div class="zeit-ast meilenstein zeit-final"> <div class="zeit-ast meilenstein zeit-final">
<div class="zeit-node" aria-hidden="true"></div> <div class="zeit-node" aria-hidden="true"></div>
<div class="zeit-karte"> <div class="zeit-karte">
@@ -238,8 +237,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-zeitreise.js"></script> <script src="assets/js/i18n-zeitreise.js?v=20260821n"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821n"></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