Arbeit der parallelen Session in git gesichert (war nur auf dem Server)

Diese Änderungen liefen bereits live auf dem Server, lagen dort aber
ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy
(stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen.
Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird.

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

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-21 17:35:14 +02:00
co-authored by Claude Opus 5
parent 4b294f6c6b
commit 5d0ea11570
65 changed files with 992 additions and 139 deletions
+6
View File
@@ -1566,6 +1566,12 @@ header.site-header {
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
.btn-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }
/* Kurzes "✓ Link kopiert"-Feedback für dogiTeilen()-Knöpfe (Zwischenablage-
Fallback ohne Web-Share-API) -- 21.08.2026. Gleiche Erfolgsfarbe wie
.vw-copy-btn.kopiert in der Verwaltung, für ein konsistentes Gefühl
zwischen öffentlicher Seite und Admin-Bereich. */
.dogi-teilen-kopiert { background: rgba(52,211,153,.15) !important; border-color: rgba(52,211,153,.5) !important; color: #5eeaa7 !important; }
/* ---------- Hero ---------- */
.hero {
position: relative;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 356 KiB

+69 -2
View File
@@ -126,10 +126,62 @@ window.I18N_PAGE = {
de: "Registrieren", "de-CH": "Registriere",
en: "Register", fr: "S'inscrire", pt: "Registrar",
},
ab_tab_register: {
de: "Neu registrieren", "de-CH": "Nöi registriere",
en: "Sign up", fr: "S'inscrire", pt: "Registrar",
},
ab_tab_login: {
de: "Schon registriert? Einloggen", "de-CH": "Scho registriert? Yylogge",
en: "Already registered? Log in", fr: "Déjà inscrit ? Se connecter", pt: "Já registrado? Entrar",
},
ab_switch_login: {
de: "Schon Supporter? Hier einloggen", "de-CH": "Scho Supporter? Da yylogge",
en: "Already a supporter? Log in here", fr: "Déjà supporter ? Connecte-toi ici", pt: "Já é apoiador? Faça login aqui",
},
ab_feld_code_neu: {
de: "Denk dir jetzt deinen Zugangscode aus (mindestens 6 Zeichen)",
"de-CH": "Dänk der jetz din Zuegangscode us (mindeschtens 6 Zeiche)",
en: "Now choose your own access code (at least 6 characters)",
fr: "Choisis maintenant ton code d'accès (au moins 6 caractères)",
pt: "Agora escolha seu código de acesso (mínimo 6 caracteres)",
},
ab_feld_code_platzhalter: {
de: "Frei wählbar — z. B. DogiCrew2026",
"de-CH": "Frei wählbar — z. B. DogiCrew2026",
en: "Anything you like — e.g. DogiCrew2026",
fr: "Comme tu veux — p. ex. DogiCrew2026",
pt: "À sua escolha — ex. DogiCrew2026",
},
ab_code_hinweis_neu: {
de: "Mit diesem Code loggst du dich künftig ein — merk ihn dir gut.",
"de-CH": "Mit däm Code loggsch di künftig y — merk der en guet.",
en: "You'll use this code to log in from now on — keep it safe.",
fr: "Tu utiliseras ce code pour te connecter — garde-le en lieu sûr.",
pt: "Você usará este código para entrar a partir de agora — guarde-o bem.",
},
ab_feld_code_eigen: {
de: "Dein Zugangscode", "de-CH": "Din Zuegangscode",
en: "Your access code", fr: "Ton code d'accès", pt: "Seu código de acesso",
},
ab_btn_login: {
de: "Einloggen", "de-CH": "Yylogge",
en: "Log in", fr: "Se connecter", pt: "Entrar",
},
ab_code_vergessen: {
de: "Code vergessen?", "de-CH": "Code vergässe?",
en: "Forgot your code?", fr: "Code oublié ?", pt: "Esqueceu seu código?",
},
ab_neuer_code_hinweis: {
de: "Fast fertig — leg jetzt deinen neuen Zugangscode fest.",
"de-CH": "Fascht fertig — leg jetz din nöie Zuegangscode fescht.",
en: "Almost done — now set your new access code.",
fr: "Presque fini — définis maintenant ton nouveau code d'accès.",
pt: "Quase pronto — agora defina seu novo código de acesso.",
},
ab_btn_code_speichern: {
de: "Zugangscode speichern", "de-CH": "Zuegangscode spichere",
en: "Save access code", fr: "Enregistrer le code d'accès", pt: "Salvar código de acesso",
},
ab_btn_login_code: {
de: "Login-Code anfordern", "de-CH": "Login-Code aafordere",
en: "Request login code", fr: "Demander un code de connexion", pt: "Solicitar código de login",
@@ -151,8 +203,23 @@ window.I18N_PAGE = {
en: "Confirm", fr: "Confirmer", pt: "Confirmar",
},
ab_eingeloggt_als: {
de: "Eingeloggt. Noch ein letzter Schritt:", "de-CH": "Yyglogget. No en letschte Schritt:",
en: "Logged in. One last step:", fr: "Connecté. Une dernière étape :", pt: "Conectado. Um último passo:",
de: "Eingeloggt.", "de-CH": "Yyglogget.",
en: "Logged in.", fr: "Connecté.", pt: "Conectado.",
},
ab_subscribe_locked_hinweis: {
de: "Dein Konto ist bereit — die Bezahlung startet, sobald das Abo offiziell live geht. Schau einfach bald wieder vorbei.",
"de-CH": "Dis Konto isch parat — d Zahlig startet, sobald s Abo offiziell live gaht. Schau eifach bald wieder verbi.",
en: "Your account is ready — payment opens as soon as the subscription officially launches. Just check back soon.",
fr: "Ton compte est prêt — le paiement s'ouvrira dès le lancement officiel de l'abonnement. Repasse bientôt.",
pt: "Sua conta está pronta — o pagamento abre assim que a assinatura for lançada oficialmente. Volte em breve.",
},
ab_weiter_startseite: {
de: "Zur Startseite →", "de-CH": "Zur Startsiite →",
en: "To the homepage →", fr: "Vers l'accueil →", pt: "Para a página inicial →",
},
ab_weiter_streamplan: {
de: "Zum Streamplan →", "de-CH": "Zum Streamplan →",
en: "To the stream schedule →", fr: "Vers le programme des lives →", pt: "Para a programação de lives →",
},
ab_oder_email: {
de: "oder mit E-Mail", "de-CH": "oder mit E-Mail",
+4
View File
@@ -97,6 +97,10 @@ window.I18N_PAGE = {
de: "Direkt ansehen →", "de-CH": "Direkt aluege →",
en: "View directly →", fr: "Voir directement →", pt: "Ver diretamente →",
},
idx_jahres_event_modal_teilen: {
de: "📤 Teilen", "de-CH": "📤 Teile",
en: "📤 Share", fr: "📤 Partager", pt: "📤 Compartilhar",
},
idx_char_eyebrow: {
de: "🩵 Die Gesichter der Welten", "de-CH": "🩵 D Gsichter vo de Wälte",
+4
View File
@@ -58,6 +58,10 @@ window.I18N_PAGE = {
de: "Mehr erfahren →", "de-CH": "Mehr erfahre →",
en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →",
},
sp_radar_teilen: {
de: "📤 Teilen", "de-CH": "📤 Teile",
en: "📤 Share", fr: "📤 Partager", pt: "📤 Compartilhar",
},
sp_card1_tag: {
de: "Fixer Termin", "de-CH": "Fixe Termin",
en: "Fixed Time", fr: "Rendez-vous fixe", pt: "Horário Fixo",
+11
View File
@@ -58,6 +58,17 @@ window.I18N_PAGE = {
de: "Jetzt abonnieren", "de-CH": "Jetzt abonniere",
en: "Subscribe now", fr: "S'abonner maintenant", pt: "Assine agora",
},
su_kein_abo_soon: {
de: "Coming soon…", "de-CH": "Coming soon…",
en: "Coming soon…", fr: "Coming soon…", pt: "Coming soon…",
},
su_kein_abo_hinweis: {
de: "Alles hier ist schon für dich vorbereitet — sobald das Abo startet, kannst du es mit einem Klick aktivieren.",
"de-CH": "Alles da isch scho für di parat — sobald s Abo startet, chasch es mit eim Klick aktiviere.",
en: "Everything here is already set up for you — as soon as the subscription starts, you can activate it with one click.",
fr: "Tout est déjà prêt pour toi — dès le lancement de l'abonnement, tu pourras l'activer en un clic.",
pt: "Tudo aqui já está preparado para você — assim que a assinatura começar, você poderá ativá-la com um clique.",
},
su_fenster2_h2: {
de: "Mein aktueller Fortschritt", "de-CH": "Mis aktuelle Fortschritt",
en: "My Current Progress", fr: "Ma progression actuelle", pt: "Meu progresso atual",
+58 -7
View File
@@ -518,16 +518,24 @@
}
const token = localStorage.getItem("supporterToken");
const status = localStorage.getItem("supporterSubStatus");
if (!token) {
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);
if (hatBereich) {
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="abonnieren.html"><span>${t("nav_cta_abo_aktivieren")}</span>${soonBadge()}</a>`;
/* Nutzer-Wunsch 21.08.2026: "meine seite ändert sich nicht und es gibt
keinen button für registrierte sich normal anzumelden und abzumelden.
wo sie die speziellen sachen sehen als supporter und dass sie auch
schon den abo sehen aber gesperrt ist." Vorher landeten Leute mit
Konto, aber ohne bezahltes Abo (status "none"), im Nav-Knopf wieder
nur bei abonnieren.html -- man merkte dem Knopf also nie an, dass man
überhaupt eingeloggt ist. supporter.html zeigt für genau diesen Fall
schon lange den Bereich "Mein Abo" -> #supp-keine-abo ("Du hast noch
kein aktives Abo" + Knopf) UND den Ausloggen-Knopf (siehe dort,
#btn-logout) -- das musste also nur noch verlinkt werden. Ab jetzt:
JEDER mit gültigem Token (egal ob "none", "active", "pending", ...)
geht auf supporter.html und sieht dort automatisch den passenden
Zustand samt Logout. */
const name = localStorage.getItem("supporterDisplayName");
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`;
}
function renderHeader() {
@@ -1172,6 +1180,49 @@
return feld[sprache] || feld.de || "";
};
/* =====================================================================
dogiTeilen() — 21.08.2026, Nutzer-Wunsch: "die events des jahres und
speziellen live events soll man auch teilen können." Wiederverwendbare
Teilen-Funktion für einzelne Inhalte (aktuell: Event des Jahres auf
index.html, spezielles Live-Event auf streamplan.html — künftig bei
Bedarf für weitere Stellen nutzbar, ohne den Mechanismus neu zu bauen).
Nutzt zuerst die native Teilen-Funktion des Geräts (Web-Share-API) —
auf dem Handy öffnet das den echten Teilen-Dialog (WhatsApp, TikTok,
Mail, ...). Die meisten Desktop-Browser kennen diese API nicht/zeigen
dort ohnehin kein sinnvolles Menü -- dort stattdessen Fallback: Link in
die Zwischenablage kopieren, der klickende Knopf bekommt kurz ein
"✓ Link kopiert"-Feedback (eigener Text + eigene Klasse, gleiches
Timing wie .vw-copy-btn in der Verwaltung). */
window.dogiTeilen = async function (daten, knopf) {
const payload = { title: daten.title || "", text: daten.text || "", url: daten.url || location.href };
if (navigator.share) {
try {
await navigator.share(payload);
return;
} catch (err) {
if (err && err.name === "AbortError") return; // Dialog selbst abgebrochen -- kein Fallback nötig
// sonst: weiter zum Zwischenablage-Fallback unten
}
}
if (!navigator.clipboard) return;
try {
await navigator.clipboard.writeText(payload.url);
if (knopf) {
const alterText = knopf.textContent;
knopf.textContent = "✓ Link kopiert";
knopf.classList.add("dogi-teilen-kopiert");
setTimeout(() => {
knopf.textContent = alterText;
knopf.classList.remove("dogi-teilen-kopiert");
}, 1600);
}
} catch {
/* Zwischenablage nicht erreichbar (z.B. kein HTTPS-Kontext) -- kein
Blocker, der Link bleibt über die normale Adressleiste erreichbar. */
}
};
/* =====================================================================
dogiTeamLaden() — 21.08.2026, Nutzer-Wunsch: "überall wo Modis sind
oder Scouts oder Manager, oder Creator, will ich dass ich die easy
+21
View File
@@ -156,7 +156,9 @@
const seCountdown = document.getElementById("se-radar-countdown");
const seTitel = document.getElementById("se-radar-titel");
const seLink = document.getElementById("se-radar-link");
const seTeilenBtn = document.getElementById("se-radar-teilen");
let zielDatum = null;
let aktuellesEvent = null; // für dogiTeilen() beim Klick (21.08.2026)
let countdownIntervalId = null;
let zeigerIntervalId = null;
@@ -201,6 +203,7 @@
return;
}
zielDatum = new Date(event.zieldatum);
aktuellesEvent = event;
seWrap.hidden = false;
seRadar.classList.remove("ist-live");
seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || "";
@@ -226,5 +229,23 @@
document.addEventListener("dogi-sprache-geaendert", () => {
if (zielDatum) specialEventLaden();
});
// Teilen (21.08.2026, Nutzer-Wunsch: "... speziellen live events soll man
// auch teilen können") -- teilt die Streamplan-Seite selbst (dort steht
// ja bereits automatisch das aktuell aktive Event), zusammen mit Titel
// im Text, damit klar ist, worum es geht. Nur EIN spezielles Event kann
// je aktiv sein, deshalb reicht ein einziger Knopf ohne Slot-Bezug.
seTeilenBtn?.addEventListener("click", () => {
if (!aktuellesEvent) return;
const titel = window.dogiFeld ? window.dogiFeld(aktuellesEvent.titel) : aktuellesEvent.titel?.de || "";
window.dogiTeilen?.(
{
title: titel,
text: `🔴 ${titel} — spezielles Live-Event bei DogFather!`,
url: `${location.origin}/streamplan.html`,
},
seTeilenBtn
);
});
}
})();