Rollenbasiertes Postfach, große Startseite, Casper-Fix, Streamplan/TikTok-Korrekturen
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@
|
||||
label: "HasiDog",
|
||||
children: [
|
||||
{ label: "HasiDog-Welt", href: "hasidog.html" },
|
||||
{ label: "Kasper", href: "kasper.html" },
|
||||
{ label: "Casper", href: "casper.html" },
|
||||
{ label: "Community", href: "community.html" },
|
||||
{ label: "Stimmen", href: "stimmen.html" },
|
||||
],
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Kasper — DOGFATHER UNIVERSE</title>
|
||||
<meta name="description" content="Kasper (Husky Casper): das tierische Gesicht der HasiDog-Welt." />
|
||||
<title>Casper — DOGFATHER UNIVERSE</title>
|
||||
<meta name="description" content="Der Husky an Dogfathers Seite — das tierische Gesicht der HasiDog-Welt." />
|
||||
<link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="DOGFATHER UNIVERSE" />
|
||||
<meta property="og:title" content="Kasper — DOGFATHER UNIVERSE" />
|
||||
<meta property="og:description" content="Kasper (Husky Casper): das tierische Gesicht der HasiDog-Welt." />
|
||||
<meta property="og:title" content="Casper — DOGFATHER UNIVERSE" />
|
||||
<meta property="og:description" content="Der Husky an Dogfathers Seite — das tierische Gesicht der HasiDog-Welt." />
|
||||
<meta property="og:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
|
||||
<meta property="og:url" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/kasper.html" />
|
||||
<meta property="og:url" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/casper.html" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Kasper — DOGFATHER UNIVERSE" />
|
||||
<meta name="twitter:description" content="Kasper (Husky Casper): das tierische Gesicht der HasiDog-Welt." />
|
||||
<meta name="twitter:title" content="Casper — DOGFATHER UNIVERSE" />
|
||||
<meta name="twitter:description" content="Der Husky an Dogfathers Seite — das tierische Gesicht der HasiDog-Welt." />
|
||||
<meta name="twitter:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
|
||||
|
||||
<link rel="stylesheet" href="assets/css/main.css" />
|
||||
@@ -29,7 +29,7 @@
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Welt 2 — HasiDog</span>
|
||||
<h1>Kasper 🐾</h1>
|
||||
<h1>Casper 🐾</h1>
|
||||
<p class="lead worlds">Der Husky an Dogfathers Seite — Namensgeber vieler Herzen in der Community.</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -40,7 +40,7 @@
|
||||
Foto: Casper<br><span class="small">Foto folgt (assets/img/casper-1.jpg — ggf. aus HEIC konvertieren)</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Wer ist Kasper?</h2>
|
||||
<h2>Wer ist Casper?</h2>
|
||||
<p>Husky Casper ist einer von Dogfathers treuen Begleitern und längst Teil der Marke geworden. Zusammen mit Kater Simba gehört er zur „Familie" hinter der Kamera.</p>
|
||||
<p>Die gemeinsame Geschichte von Dogfather und Casper gibt es sogar als Buch: <strong>„Hasidog & Casper"</strong>.</p>
|
||||
</div>
|
||||
@@ -58,10 +58,26 @@ und `postfach.html` kann sie mit dem in Schritt 3 gesetzten Code anzeigen.
|
||||
|
||||
## Wer den Code kennen darf
|
||||
|
||||
Der Code aus Schritt 3 ist der einzige Schlüssel zum Postfach. Nur an Personen weitergeben, die
|
||||
wirklich Zugriff auf Bewerbungen haben sollen. Code ändern: einfach `wrangler secret put
|
||||
POSTFACH_CODE` erneut ausführen und neuen Wert eingeben — alte Bewerbungen bleiben erhalten,
|
||||
nur der Zugangscode ändert sich.
|
||||
Der Code aus Schritt 3 ist dein **Owner-Code** — nur du (Dogi) solltest ihn kennen. Damit kannst
|
||||
du nicht nur Bewerbungen sehen, sondern auch Helfer-Codes für andere Personen vergeben (siehe
|
||||
unten). Code ändern: einfach `wrangler secret put POSTFACH_CODE` erneut ausführen und neuen Wert
|
||||
eingeben — alte Bewerbungen bleiben erhalten, nur der Zugangscode ändert sich.
|
||||
|
||||
## Rollen für Rechte Hand, Linke Hand & Co.
|
||||
|
||||
Auf `postfach.html` gibt es — nur sichtbar, wenn du dich mit deinem Owner-Code einloggst — einen
|
||||
Bereich „Rollen verwalten". Dort kannst du:
|
||||
|
||||
- **Neue Codes erzeugen**, mit Rolle (Rechte Hand / Linke Hand / frei) + Name (z.B. "VanVan").
|
||||
Der neue Code wird **einmalig** angezeigt — sofort weitergeben/notieren, danach ist er nicht
|
||||
mehr abrufbar (nur als Hash gespeichert, aus Sicherheitsgründen).
|
||||
- **Codes entziehen**, falls sich jemandes Rolle ändert oder der Zugang nicht mehr gebraucht wird.
|
||||
|
||||
**Was Helfer-Codes können:** ausschließlich die eingegangenen Bewerbungen ansehen — dieselbe
|
||||
Ansicht wie du. **Was sie NICHT können:** neue Codes erzeugen, andere Codes entziehen, oder
|
||||
irgendetwas an der Website selbst verändern/löschen. Für Website-Änderungen gibt es in diesem
|
||||
Worker schlicht keine einzige Funktion — das ist technisch gar nicht möglich, egal welchen Code
|
||||
jemand hat.
|
||||
|
||||
## Wartung
|
||||
|
||||
|
||||
+146
-13
@@ -1,20 +1,38 @@
|
||||
/* =====================================================================
|
||||
DOGFATHER UNIVERSE — Bewerbungs-Postfach Worker
|
||||
Nimmt Bewerbungen von bewerben.html entgegen, speichert sie in KV,
|
||||
und gibt sie nur nach korrektem Code (serverseitig geprüft, per
|
||||
Secret gespeichert) an postfach.html zurück.
|
||||
und gibt sie nur nach korrektem Code (serverseitig geprüft) an
|
||||
postfach.html zurück.
|
||||
|
||||
Zugriffs-Modell (wichtig für's Verständnis):
|
||||
- Owner-Code (POSTFACH_CODE, Secret) = Dogi selbst. Sieht alle
|
||||
Bewerbungen UND kann Helfer-Codes mit Rollen (z.B. "Rechte Hand",
|
||||
"Linke Hand") anlegen/entziehen.
|
||||
- Helfer-Codes (von Dogi vergeben) = sehen NUR die Bewerbungen.
|
||||
Sie können KEINE neuen Codes anlegen, KEINE Rollen verwalten,
|
||||
und haben KEINERLEI Möglichkeit, irgendetwas an der Website
|
||||
selbst zu verändern oder zu löschen — dafür gibt es in diesem
|
||||
Worker schlicht keine Funktion. Er kann ausschließlich
|
||||
Bewerbungen entgegennehmen und (mit gültigem Code) anzeigen.
|
||||
- Alle Codes werden nur als SHA-256-Hash in KV gespeichert, nie im
|
||||
Klartext. Ein neu erzeugter Code wird genau einmal zurückgegeben
|
||||
(beim Erstellen) — danach ist er nicht mehr abrufbar.
|
||||
|
||||
Endpoints:
|
||||
POST /submit { type: "creator"|"scout"|"kooperation", data: {...} }
|
||||
POST /postfach { code: "..." } -> Liste aller Bewerbungen
|
||||
GET /health -> einfacher Check ob der Worker läuft
|
||||
POST /submit { type, data } -> Bewerbung speichern
|
||||
POST /postfach { code } -> Bewerbungen ansehen
|
||||
POST /admin/roles/list { ownerCode } -> Helfer-Codes auflisten (nur Owner)
|
||||
POST /admin/roles/create { ownerCode, role, label } -> neuen Helfer-Code erzeugen (nur Owner)
|
||||
POST /admin/roles/revoke { ownerCode, keyId } -> Helfer-Code entziehen (nur Owner)
|
||||
GET /health -> einfacher Check ob der Worker läuft
|
||||
|
||||
Secrets/Bindings (siehe wrangler.toml + README.md):
|
||||
KV Binding: BEWERBUNGEN (Cloudflare KV Namespace)
|
||||
KV Binding: BEWERBUNGEN (Cloudflare KV Namespace, wird auch für Rollen genutzt)
|
||||
Secret: POSTFACH_CODE (mit `wrangler secret put POSTFACH_CODE` setzen)
|
||||
===================================================================== */
|
||||
|
||||
const ALLOWED_TYPES = ["creator", "scout", "kooperation"];
|
||||
const ROLE_KEY_PREFIX = "role:";
|
||||
|
||||
function corsHeaders() {
|
||||
// TODO: sobald die Domain final steht, statt "*" die echte Domain eintragen
|
||||
@@ -34,6 +52,30 @@ function json(data, status = 200) {
|
||||
});
|
||||
}
|
||||
|
||||
async function sha256Hex(text) {
|
||||
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
||||
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function generateFriendlyCode() {
|
||||
// z.B. "K3F9-QX2L-7HRT" — leicht abzutippen/vorzulesen, ausreichend Entropie
|
||||
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne verwechselbare Zeichen (I,O,0,1)
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(12));
|
||||
let raw = "";
|
||||
for (const b of bytes) raw += alphabet[b % alphabet.length];
|
||||
return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`;
|
||||
}
|
||||
|
||||
async function isOwnerCode(code, env) {
|
||||
return !!env.POSTFACH_CODE && code === env.POSTFACH_CODE;
|
||||
}
|
||||
|
||||
async function findHelperRole(code, env) {
|
||||
const hash = await sha256Hex(code);
|
||||
const raw = await env.BEWERBUNGEN.get(ROLE_KEY_PREFIX + hash);
|
||||
return raw ? { keyId: ROLE_KEY_PREFIX + hash, ...JSON.parse(raw) } : null;
|
||||
}
|
||||
|
||||
async function handleSubmit(request, env) {
|
||||
let body;
|
||||
try {
|
||||
@@ -47,7 +89,6 @@ async function handleSubmit(request, env) {
|
||||
return json({ ok: false, error: "Ungültige Bewerbungsdaten." }, 400);
|
||||
}
|
||||
|
||||
// simple Server-seitige Validierung: keine leeren Kernfelder
|
||||
const values = Object.values(data).map((v) => String(v || "").trim());
|
||||
if (values.every((v) => v === "")) {
|
||||
return json({ ok: false, error: "Leere Bewerbung." }, 400);
|
||||
@@ -57,10 +98,7 @@ async function handleSubmit(request, env) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const key = `sub:${timestamp}:${id}`;
|
||||
|
||||
await env.BEWERBUNGEN.put(
|
||||
key,
|
||||
JSON.stringify({ id, type, data, timestamp })
|
||||
);
|
||||
await env.BEWERBUNGEN.put(key, JSON.stringify({ id, type, data, timestamp }));
|
||||
|
||||
return json({ ok: true });
|
||||
}
|
||||
@@ -74,7 +112,14 @@ async function handlePostfach(request, env) {
|
||||
}
|
||||
|
||||
const code = (body && body.code) || "";
|
||||
if (!env.POSTFACH_CODE || code !== env.POSTFACH_CODE) {
|
||||
let owner = await isOwnerCode(code, env);
|
||||
let helperRole = null;
|
||||
|
||||
if (!owner) {
|
||||
helperRole = await findHelperRole(code, env);
|
||||
}
|
||||
|
||||
if (!owner && !helperRole) {
|
||||
// bewusst generische Fehlermeldung, kein Hinweis ob Code existiert
|
||||
return json({ ok: false, error: "Falscher Code." }, 401);
|
||||
}
|
||||
@@ -91,7 +136,83 @@ async function handlePostfach(request, env) {
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); // neueste zuerst
|
||||
|
||||
return json({ ok: true, bewerbungen });
|
||||
return json({
|
||||
ok: true,
|
||||
isOwner: owner,
|
||||
roleLabel: owner ? "Dogi (Owner)" : helperRole.label,
|
||||
bewerbungen,
|
||||
});
|
||||
}
|
||||
|
||||
async function requireOwner(body, env) {
|
||||
const ownerCode = (body && body.ownerCode) || "";
|
||||
return isOwnerCode(ownerCode, env);
|
||||
}
|
||||
|
||||
async function handleRolesList(request, env) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return json({ ok: false, error: "Ungültiges JSON." }, 400);
|
||||
}
|
||||
if (!(await requireOwner(body, env))) {
|
||||
return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401);
|
||||
}
|
||||
|
||||
const list = await env.BEWERBUNGEN.list({ prefix: ROLE_KEY_PREFIX, limit: 200 });
|
||||
const roles = await Promise.all(
|
||||
list.keys.map(async (k) => {
|
||||
const raw = await env.BEWERBUNGEN.get(k.name);
|
||||
return raw ? { keyId: k.name, ...JSON.parse(raw) } : null;
|
||||
})
|
||||
);
|
||||
|
||||
return json({ ok: true, roles: roles.filter(Boolean) });
|
||||
}
|
||||
|
||||
async function handleRolesCreate(request, env) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return json({ ok: false, error: "Ungültiges JSON." }, 400);
|
||||
}
|
||||
if (!(await requireOwner(body, env))) {
|
||||
return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401);
|
||||
}
|
||||
|
||||
const role = String((body && body.role) || "").slice(0, 60).trim() || "Helfer";
|
||||
const label = String((body && body.label) || "").slice(0, 80).trim() || role;
|
||||
|
||||
const newCode = generateFriendlyCode();
|
||||
const hash = await sha256Hex(newCode);
|
||||
const record = { role, label, createdAt: new Date().toISOString() };
|
||||
|
||||
await env.BEWERBUNGEN.put(ROLE_KEY_PREFIX + hash, JSON.stringify(record));
|
||||
|
||||
// Der Klartext-Code wird NUR jetzt einmalig zurückgegeben — danach nie wieder.
|
||||
return json({ ok: true, code: newCode, ...record });
|
||||
}
|
||||
|
||||
async function handleRolesRevoke(request, env) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return json({ ok: false, error: "Ungültiges JSON." }, 400);
|
||||
}
|
||||
if (!(await requireOwner(body, env))) {
|
||||
return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401);
|
||||
}
|
||||
|
||||
const keyId = String((body && body.keyId) || "");
|
||||
if (!keyId.startsWith(ROLE_KEY_PREFIX)) {
|
||||
return json({ ok: false, error: "Ungültige keyId." }, 400);
|
||||
}
|
||||
|
||||
await env.BEWERBUNGEN.delete(keyId);
|
||||
return json({ ok: true });
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -114,6 +235,18 @@ export default {
|
||||
return handlePostfach(request, env);
|
||||
}
|
||||
|
||||
if (url.pathname === "/admin/roles/list" && request.method === "POST") {
|
||||
return handleRolesList(request, env);
|
||||
}
|
||||
|
||||
if (url.pathname === "/admin/roles/create" && request.method === "POST") {
|
||||
return handleRolesCreate(request, env);
|
||||
}
|
||||
|
||||
if (url.pathname === "/admin/roles/revoke" && request.method === "POST") {
|
||||
return handleRolesRevoke(request, env);
|
||||
}
|
||||
|
||||
return json({ ok: false, error: "Not found." }, 404);
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@
|
||||
<div>
|
||||
<h2>Wer ist Dogfather?</h2>
|
||||
<p class="lead">Content Creator, Streamer auf TikTok (<strong>@dogfather0804</strong>) und das Herz einer wachsenden Community.</p>
|
||||
<p>An seiner Seite: Husky <a href="kasper.html">Casper</a> und Kater Simba — treue Begleiter, die längst Teil der Marke geworden sind. Mit seinem Core-Team baut Dogfather jeden Tag daran, dass sich Menschen in seiner Community nicht allein fühlen.</p>
|
||||
<p>An seiner Seite: Husky <a href="casper.html">Casper</a> und Kater Simba — treue Begleiter, die längst Teil der Marke geworden sind. Mit seinem Core-Team baut Dogfather jeden Tag daran, dass sich Menschen in seiner Community nicht allein fühlen.</p>
|
||||
<div class="todo-note">
|
||||
<strong>Hinweis zur Privatsphäre:</strong> Auf dieser Website wird ausschließlich der Persona-Name „Dogfather / Dogi" veröffentlicht — keine echten bürgerlichen Daten (kein Nachname, kein genaues Alter, keine genaue Herkunft).
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@
|
||||
<h2>Was ist HasiDog?</h2>
|
||||
<p>HasiDog ist mehr als ein Spitzname — es ist die verspielte, familiäre Marke rund um Dogfathers Community. Unter dem Namen „Hasidog" erscheint auch die eigene Musik auf Spotify.</p>
|
||||
<div class="btn-row">
|
||||
<a class="btn btn-primary" href="kasper.html">Zur Kasper-Seite 🐾</a>
|
||||
<a class="btn btn-primary" href="casper.html">Zur Casper-Seite 🐾</a>
|
||||
<a class="btn btn-outline" href="community.html">Zur Community</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
<h2 class="text-center">Die HasiDog-Familie</h2>
|
||||
<div class="grid grid-3">
|
||||
<div class="card text-center"><h3>🎵 Musik</h3><p>Songs unter dem Namen „Hasidog" auf Spotify.</p></div>
|
||||
<div class="card text-center"><h3>🐕 Casper</h3><p>Der Husky, der zum Gesicht der HasiDog-Welt wurde — eigene Seite: <a href="kasper.html">Kasper</a>.</p></div>
|
||||
<div class="card text-center"><h3>🐕 Casper</h3><p>Der Husky, der zum Gesicht der HasiDog-Welt wurde — eigene Seite: <a href="casper.html">Casper</a>.</p></div>
|
||||
<div class="card text-center"><h3>📖 Buch</h3><p>„Hasidog & Casper" — die Geschichte in Buchform.</p></div>
|
||||
</div>
|
||||
<div class="todo-note">
|
||||
|
||||
+133
-18
@@ -26,11 +26,18 @@
|
||||
<div id="site-header"></div>
|
||||
|
||||
<main>
|
||||
<!-- ================= HERO ================= -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<span class="eyebrow">🩵 DOGFATHER UNIVERSE</span>
|
||||
<h1>Streamer. Community. HasiDog. Manager.<br>Gemeinsam stark.</h1>
|
||||
<p class="lead worlds">Drei Welten, eine Familie: Team Dogi.</p>
|
||||
|
||||
<div class="card" style="max-width:420px;margin:1.4rem auto 0;padding:1rem 1.4rem;">
|
||||
<span class="badge rank-1">🔴 Live-Status</span>
|
||||
<p class="small" style="margin:.5rem 0 0;">Dogfather geht <strong>jeden Tag ab 20 Uhr</strong> live — oft schon tagsüber, zu wechselnden Zeiten. <a href="streamplan.html">Streamplan ansehen →</a></p>
|
||||
</div>
|
||||
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-primary" href="links.html">Alle Links 🩵</a>
|
||||
<a class="btn btn-outline" href="bewerben.html">Teil vom Team werden</a>
|
||||
@@ -56,39 +63,147 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= STATS ================= -->
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<div class="grid grid-3">
|
||||
<div class="card text-center">
|
||||
<h3>🐾 Team Dogi ist Familie</h3>
|
||||
<p>Unsere Mission: Jeder soll lächeln und sich nicht allein fühlen.</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<h3>🎥 TikTok @dogfather0804</h3>
|
||||
<p>Halligalli, Eindeutig Zweideutig & Hasencontent — Content mit Herz und Humor.</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<h3>🎵 Musik: Hasidog & Van-Van</h3>
|
||||
<p>Eigene Musikprojekte auf Spotify — von Dogfather und VanVan.</p>
|
||||
</div>
|
||||
<div class="grid grid-4" id="stats-bar">
|
||||
<div class="card text-center"><h3 id="stat-team">–</h3><p class="small muted">Menschen im Team Dogi</p></div>
|
||||
<div class="card text-center"><h3>Täglich</h3><p class="small muted">live ab 20 Uhr</p></div>
|
||||
<div class="card text-center"><h3>3</h3><p class="small muted">Welten, eine Familie</p></div>
|
||||
<div class="card text-center"><h3>🩵</h3><p class="small muted">Team Dogi ist Familie</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= WAS DICH ERWARTET ================= -->
|
||||
<section>
|
||||
<div class="container">
|
||||
<span class="eyebrow text-center" style="display:block;text-align:center;">Ein bisschen von allem</span>
|
||||
<h2 class="text-center">Was dich im Universum erwartet</h2>
|
||||
<div class="grid grid-4">
|
||||
<a class="card" href="streamer.html"><h3>🎥 Streamer</h3><p class="small">Halligalli, Humor & Hasencontent.</p></a>
|
||||
<a class="card" href="zeitreise.html"><h3>🕰️ Zeitreise</h3><p class="small">Die Geschichte von Dogfather & Team Dogi.</p></a>
|
||||
<a class="card" href="community.html"><h3>🩵 Community</h3><p class="small">Team Dogi ist Familie.</p></a>
|
||||
<a class="card" href="casper.html"><h3>🐾 Casper</h3><p class="small">Der Husky an Dogfathers Seite.</p></a>
|
||||
<a class="card" href="team-modis.html"><h3>🧑🤝🧑 Modi-Team</h3><p class="small">VanVan, Diene & Co.</p></a>
|
||||
<a class="card" href="aktionen.html"><h3>🎁 Aktionen</h3><p class="small">Projekte, Kooperationen, Tierwohl.</p></a>
|
||||
<a class="card" href="medien.html"><h3>🖼️ Galerie</h3><p class="small">Fotos aus allen drei Welten.</p></a>
|
||||
<a class="card" href="videos.html"><h3>▶️ Videos</h3><p class="small">Die besten Highlights.</p></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= TEAM PREVIEW ================= -->
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center">Team Dogi 🩵</h2>
|
||||
<p class="lead text-center" style="max-width:60ch;margin:0 auto;">Von VanVan bis zu den Scouts — die Menschen, die DOGFATHER UNIVERSE tragen.</p>
|
||||
<div id="team-preview" class="grid grid-4"></div>
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-outline" href="team-modis.html">Modi-Team ansehen</a>
|
||||
<a class="btn btn-outline" href="team-scouts.html">Scout-Team ansehen</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= AKTIONEN TEASER ================= -->
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2 class="text-center">Was gerade läuft</h2>
|
||||
<div id="aktionen-teaser" class="grid grid-3"></div>
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-outline" href="aktionen.html">Alle Aktionen & Projekte</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= GALERIE TEASER ================= -->
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center">Impressionen</h2>
|
||||
<div class="grid grid-4">
|
||||
<div class="img-placeholder">Foto folgt</div>
|
||||
<div class="img-placeholder">Foto folgt</div>
|
||||
<div class="img-placeholder">Foto folgt</div>
|
||||
<div class="img-placeholder">Foto folgt</div>
|
||||
</div>
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-outline" href="medien.html">Zur ganzen Galerie</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= TESTIMONIAL ================= -->
|
||||
<section>
|
||||
<div class="container text-center">
|
||||
<span class="eyebrow">Neu in der Community?</span>
|
||||
<h2>Schau dir an, wer Team Dogi ausmacht</h2>
|
||||
<p class="lead" style="max-width:60ch;margin:0 auto;">Von den Scouts über die Modis bis zu VanVan — lerne die Menschen hinter Dogfather kennen.</p>
|
||||
<span class="eyebrow">Was andere sagen</span>
|
||||
<div class="card" style="max-width:640px;margin:1rem auto 0;">
|
||||
<p class="quote" style="font-size:1.15rem;">„Platzhalter-Zitat — echtes Testimonial folgt."</p>
|
||||
<p class="small muted">— Community-Mitglied</p>
|
||||
</div>
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-secondary" href="team-modis.html">Modi-Team ansehen</a>
|
||||
<a class="btn btn-outline" href="team-scouts.html">Scout-Team ansehen</a>
|
||||
<a class="btn btn-outline" href="stimmen.html">Mehr Stimmen</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= FINALER CTA ================= -->
|
||||
<section class="hero">
|
||||
<div class="container text-center">
|
||||
<span class="eyebrow">🩵 Bereit?</span>
|
||||
<h2>Werde Teil von DOGFATHER UNIVERSE</h2>
|
||||
<p class="lead" style="max-width:56ch;margin:0 auto;">Als Zuschauer:in, Creator, Scout oder Kooperationspartner — hier ist für jeden Platz.</p>
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-primary" href="links.html">Alle Links 🩵</a>
|
||||
<a class="btn btn-secondary" href="bewerben.html">Jetzt bewerben</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="site-footer"></div>
|
||||
<script src="assets/js/data-modis.js"></script>
|
||||
<script src="assets/js/data-scouts.js"></script>
|
||||
<script src="assets/js/data-aktionen.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Stats: Team-Größe aus den Datenobjekten berechnen
|
||||
const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length;
|
||||
const statTeam = document.getElementById("stat-team");
|
||||
if (statTeam) statTeam.textContent = teamCount > 0 ? `${teamCount}+` : "–";
|
||||
|
||||
// Team-Vorschau: VanVan zuerst, dann die nächsten nach Rang, plus 1-2 Scouts
|
||||
const teamPreview = document.getElementById("team-preview");
|
||||
if (teamPreview && window.MODIS) {
|
||||
const people = [...window.MODIS].sort((a, b) => a.rank - b.rank).slice(0, 4);
|
||||
teamPreview.innerHTML = people
|
||||
.map(
|
||||
(p) => `
|
||||
<div class="profile-card">
|
||||
<div class="avatar">${p.photo ? `<img src="${p.photo}" alt="${p.name}">` : p.name.slice(0, 2).toUpperCase()}</div>
|
||||
<h3>${p.name}</h3>
|
||||
<div class="role">${p.role}</div>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// 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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+170
-25
@@ -7,17 +7,6 @@
|
||||
<title>Postfach — DOGFATHER UNIVERSE</title>
|
||||
<meta name="description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
|
||||
<link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="DOGFATHER UNIVERSE" />
|
||||
<meta property="og:title" content="Postfach — DOGFATHER UNIVERSE" />
|
||||
<meta property="og:description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
|
||||
<meta property="og:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
|
||||
<meta property="og:url" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/postfach.html" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Postfach — DOGFATHER UNIVERSE" />
|
||||
<meta name="twitter:description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
|
||||
<meta name="twitter:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
|
||||
|
||||
<link rel="stylesheet" href="assets/css/main.css" />
|
||||
<link rel="stylesheet" href="assets/css/theme-dogfather.css" />
|
||||
<link rel="stylesheet" href="assets/css/theme-hasidog.css" />
|
||||
@@ -36,7 +25,7 @@
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container" style="max-width:520px;">
|
||||
<div class="container" style="max-width:520px;" id="gate-container">
|
||||
<form id="code-gate" class="app-form" novalidate>
|
||||
<div class="field">
|
||||
<label for="code-input">Zugangscode</label>
|
||||
@@ -56,6 +45,15 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Eingeloggt: Kopfzeile mit Rolle + Logout ===== -->
|
||||
<section class="section-tight" id="session-bar" style="display:none;padding-top:0;">
|
||||
<div class="container" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
|
||||
<span class="badge rank-1" id="role-badge">Eingeloggt</span>
|
||||
<button class="btn btn-outline" id="logout-btn" type="button">Abmelden</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Mailbox (für alle mit gültigem Code) ===== -->
|
||||
<section class="section-tight" id="inbox-section" style="display:none;">
|
||||
<div class="container">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;margin-bottom:1.4rem;">
|
||||
@@ -65,6 +63,40 @@
|
||||
<div id="inbox-list" class="grid grid-2"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Rollen-Verwaltung (NUR für Dogi/Owner sichtbar) ===== -->
|
||||
<section id="admin-section" style="display:none;">
|
||||
<div class="container">
|
||||
<h2 class="text-center">Rollen verwalten</h2>
|
||||
<p class="lead text-center" style="max-width:60ch;margin:0 auto;">
|
||||
Vergib Codes an Rechte Hand, Linke Hand oder wen du sonst reinlassen willst. Diese Codes
|
||||
zeigen <strong>ausschließlich die Bewerbungen</strong> — nichts an der Website selbst kann
|
||||
damit verändert oder gelöscht werden, dafür gibt es hier schlicht keine Funktion.
|
||||
</p>
|
||||
|
||||
<form id="role-create-form" class="app-form" style="max-width:520px;margin:1.6rem auto;" novalidate>
|
||||
<div class="field">
|
||||
<label for="role-select">Rolle</label>
|
||||
<select id="role-select" name="role">
|
||||
<option value="Rechte Hand">Rechte Hand</option>
|
||||
<option value="Linke Hand">Linke Hand</option>
|
||||
<option value="Helfer">Sonstige / frei</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="role-label">Für wen? (Name/Bezeichnung)</label>
|
||||
<input id="role-label" name="label" type="text" placeholder="z.B. VanVan" required />
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Neuen Code erzeugen</button>
|
||||
<div class="form-status" id="role-create-status"></div>
|
||||
</form>
|
||||
|
||||
<div class="todo-note" id="new-code-reveal" style="display:none;max-width:520px;margin:0 auto 2rem;"></div>
|
||||
|
||||
<h3 class="text-center">Bestehende Codes</h3>
|
||||
<div id="roles-list" class="grid grid-2"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="site-footer"></div>
|
||||
@@ -75,12 +107,21 @@
|
||||
const API_BASE_URL = "";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const gateContainer = document.getElementById("gate-container");
|
||||
const form = document.getElementById("code-gate");
|
||||
const status = document.getElementById("gate-status");
|
||||
const sessionBar = document.getElementById("session-bar");
|
||||
const roleBadge = document.getElementById("role-badge");
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
const inboxSection = document.getElementById("inbox-section");
|
||||
const inboxList = document.getElementById("inbox-list");
|
||||
const refreshBtn = document.getElementById("refresh-btn");
|
||||
const notConfigured = document.getElementById("not-configured-note");
|
||||
const adminSection = document.getElementById("admin-section");
|
||||
const roleCreateForm = document.getElementById("role-create-form");
|
||||
const roleCreateStatus = document.getElementById("role-create-status");
|
||||
const newCodeReveal = document.getElementById("new-code-reveal");
|
||||
const rolesList = document.getElementById("roles-list");
|
||||
|
||||
if (!API_BASE_URL) {
|
||||
notConfigured.style.display = "block";
|
||||
@@ -88,7 +129,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
notConfigured.style.display = "none";
|
||||
}
|
||||
|
||||
let lastCode = "";
|
||||
let session = { code: "", isOwner: false, roleLabel: "" };
|
||||
|
||||
function renderInbox(bewerbungen) {
|
||||
if (!bewerbungen.length) {
|
||||
@@ -111,44 +152,148 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function loadInbox(code) {
|
||||
async function loadInbox() {
|
||||
status.className = "form-status show";
|
||||
status.textContent = "Lade Postfach …";
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/postfach`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code }),
|
||||
body: JSON.stringify({ code: session.code }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok || !json.ok) {
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
status.textContent = "Falscher Code oder Postfach nicht erreichbar.";
|
||||
status.className = "form-status show error";
|
||||
inboxSection.style.display = "none";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
status.className = "form-status";
|
||||
session.isOwner = data.isOwner;
|
||||
session.roleLabel = data.roleLabel;
|
||||
|
||||
gateContainer.style.display = "none";
|
||||
sessionBar.style.display = "block";
|
||||
inboxSection.style.display = "block";
|
||||
renderInbox(json.bewerbungen);
|
||||
roleBadge.textContent = `Eingeloggt als: ${data.roleLabel}`;
|
||||
renderInbox(data.bewerbungen);
|
||||
|
||||
if (data.isOwner) {
|
||||
adminSection.style.display = "block";
|
||||
loadRoles();
|
||||
} else {
|
||||
adminSection.style.display = "none";
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
status.textContent = "Postfach gerade nicht erreichbar. Bitte später erneut versuchen.";
|
||||
status.className = "form-status show error";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener("submit", (e) => {
|
||||
async function loadRoles() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/admin/roles/list`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
rolesList.innerHTML = `<p class="muted">Konnte Codes nicht laden.</p>`;
|
||||
return;
|
||||
}
|
||||
if (!data.roles.length) {
|
||||
rolesList.innerHTML = `<p class="muted">Noch keine Helfer-Codes vergeben.</p>`;
|
||||
return;
|
||||
}
|
||||
rolesList.innerHTML = data.roles
|
||||
.map(
|
||||
(r) => `
|
||||
<div class="card">
|
||||
<span class="tag">${r.role}</span>
|
||||
<h3>${r.label}</h3>
|
||||
<p class="small muted">Erstellt: ${new Date(r.createdAt).toLocaleDateString("de-DE")}</p>
|
||||
<button class="btn btn-outline revoke-btn" data-key="${r.keyId}" type="button">Code entziehen</button>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
rolesList.querySelectorAll(".revoke-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
if (!confirm("Diesen Code wirklich entziehen? Die Person kann sich danach nicht mehr einloggen.")) return;
|
||||
await fetch(`${API_BASE_URL}/admin/roles/revoke`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code, keyId: btn.dataset.key }),
|
||||
});
|
||||
loadRoles();
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
rolesList.innerHTML = `<p class="muted">Konnte Codes nicht laden.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!API_BASE_URL) {
|
||||
status.textContent = "Postfach ist noch nicht eingerichtet (siehe Hinweis unten).";
|
||||
status.className = "form-status show error";
|
||||
return;
|
||||
}
|
||||
lastCode = document.getElementById("code-input").value;
|
||||
loadInbox(lastCode);
|
||||
session.code = document.getElementById("code-input").value;
|
||||
const ok = await loadInbox();
|
||||
if (ok) {
|
||||
status.className = "form-status";
|
||||
form.reset();
|
||||
}
|
||||
});
|
||||
|
||||
logoutBtn.addEventListener("click", () => {
|
||||
session = { code: "", isOwner: false, roleLabel: "" };
|
||||
gateContainer.style.display = "block";
|
||||
sessionBar.style.display = "none";
|
||||
inboxSection.style.display = "none";
|
||||
adminSection.style.display = "none";
|
||||
status.textContent = "";
|
||||
status.className = "form-status";
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener("click", () => {
|
||||
if (lastCode) loadInbox(lastCode);
|
||||
if (session.code) loadInbox();
|
||||
});
|
||||
|
||||
roleCreateForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const role = document.getElementById("role-select").value;
|
||||
const label = document.getElementById("role-label").value.trim();
|
||||
if (!label) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/admin/roles/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code, role, label }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
roleCreateStatus.textContent = "Konnte keinen Code erzeugen.";
|
||||
roleCreateStatus.className = "form-status show error";
|
||||
return;
|
||||
}
|
||||
newCodeReveal.style.display = "block";
|
||||
newCodeReveal.innerHTML = `
|
||||
<strong>Neuer Code für ${data.label} (${data.role}):</strong><br>
|
||||
<span style="font-size:1.3rem;font-family:monospace;letter-spacing:.05em;">${data.code}</span><br>
|
||||
<span class="small">Jetzt sichern/weitergeben — dieser Code wird nirgends mehr angezeigt!</span>`;
|
||||
roleCreateStatus.textContent = "";
|
||||
roleCreateStatus.className = "form-status";
|
||||
roleCreateForm.reset();
|
||||
loadRoles();
|
||||
} catch {
|
||||
roleCreateStatus.textContent = "Postfach gerade nicht erreichbar.";
|
||||
roleCreateStatus.className = "form-status show error";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/streamplan.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/community.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/hasidog.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/kasper.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/casper.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/aktionen.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/zeitreise.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/medien.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
|
||||
+13
-8
@@ -32,22 +32,27 @@
|
||||
<h1>Streamplan & Livebereich</h1>
|
||||
<p class="lead worlds">Wann ist Dogfather live?</p>
|
||||
<div class="btn-row" style="justify-content:center;">
|
||||
<a class="btn btn-primary" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener">Zum TikTok-Livestream</a>
|
||||
<a class="btn btn-primary" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener">Zur Dogi TikTok-Seite</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center">Wochenplan</h2>
|
||||
<div class="grid grid-4">
|
||||
<div class="card text-center"><h3>Montag</h3><p class="small muted">TODO: Uhrzeit/Format</p></div>
|
||||
<div class="card text-center"><h3>Mittwoch</h3><p class="small muted">TODO: Uhrzeit/Format</p></div>
|
||||
<div class="card text-center"><h3>Freitag</h3><p class="small muted">TODO: Uhrzeit/Format</p></div>
|
||||
<div class="card text-center"><h3>Sonntag</h3><p class="small muted">TODO: Uhrzeit/Format</p></div>
|
||||
<div class="grid grid-2" style="align-items:center;">
|
||||
<div class="card">
|
||||
<span class="tag">Fixer Termin</span>
|
||||
<h3>🌙 Jeden Tag ab 20 Uhr</h3>
|
||||
<p>Dogfather geht <strong>jeden Tag</strong> live — verlässlich ab <strong>20:00 Uhr</strong>. Das ist der feste Abend-Stream, auf den man sich einstellen kann.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="tag">Bonus</span>
|
||||
<h3>☀️ Extra-Streams tagsüber</h3>
|
||||
<p>Oft geht's auch schon tagsüber live — die Uhrzeit dafür variiert von Tag zu Tag. Am besten TikTok-Benachrichtigungen für @dogfather0804 aktivieren, um keinen Stream zu verpassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="todo-note">
|
||||
<strong>TODO:</strong> Echte Streamtage/-zeiten und Formate mit Dogi klären (Masterplan Block D) und hier eintragen.
|
||||
<strong>TODO:</strong> Streamformate, wiederkehrende Segmente und Running Gags (Masterplan Block D) hier noch ergänzen, sobald geklärt.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user