Spotify-Links, Live-Status-Feature (grüner Punkt) mit Owner-Toggle

This commit is contained in:
2026-07-30 20:39:12 +02:00
parent b1712ec33b
commit 4263564f9f
8 changed files with 149 additions and 12 deletions
+23
View File
@@ -425,6 +425,29 @@ footer.site-footer {
font-size: .8rem; color: var(--text-muted);
}
/* ---------- Live-Status-Punkt ---------- */
.live-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #6b7280; /* grau = aktuell offline */
margin-right: .45rem;
vertical-align: middle;
}
.live-dot.is-live {
background: #22c55e; /* grün = live */
animation: live-pulse 1.6s infinite;
}
@keyframes live-pulse {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, .55); }
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
@media (prefers-reduced-motion: reduce) {
.live-dot.is-live { animation: none; }
}
/* ---------- Badges / Hierarchy ---------- */
.badge { display: inline-block; font-size: .7rem; text-transform: uppercase; letter-spacing: .06em; padding: .2rem .6rem; border-radius: 999px; background: var(--bg-alt); border: 1px solid var(--border); color: var(--text-muted); }
.badge.rank-1 { border-color: var(--accent); color: var(--accent); }
+36 -1
View File
@@ -24,6 +24,8 @@
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 /live-status -> öffentlich: {live, updatedAt}
POST /live-status/set { ownerCode, live } -> Live-Status umschalten (nur Owner)
GET /health -> einfacher Check ob der Worker läuft
Secrets/Bindings (siehe wrangler.toml + README.md):
@@ -33,6 +35,7 @@
const ALLOWED_TYPES = ["creator", "scout", "kooperation"];
const ROLE_KEY_PREFIX = "role:";
const LIVE_STATUS_KEY = "meta:live";
function corsHeaders() {
// TODO: sobald die Domain final steht, statt "*" die echte Domain eintragen
@@ -40,7 +43,7 @@ function corsHeaders() {
// an die API senden kann.
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
}
@@ -215,6 +218,30 @@ async function handleRolesRevoke(request, env) {
return json({ ok: true });
}
async function handleLiveStatusGet(env) {
const raw = await env.BEWERBUNGEN.get(LIVE_STATUS_KEY);
const data = raw ? JSON.parse(raw) : { live: false, updatedAt: null };
return json({ ok: true, ...data });
}
async function handleLiveStatusSet(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 den Live-Status ändern." }, 401);
}
const live = !!(body && body.live);
const record = { live, updatedAt: new Date().toISOString() };
await env.BEWERBUNGEN.put(LIVE_STATUS_KEY, JSON.stringify(record));
return json({ ok: true, ...record });
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
@@ -247,6 +274,14 @@ export default {
return handleRolesRevoke(request, env);
}
if (url.pathname === "/live-status" && request.method === "GET") {
return handleLiveStatusGet(env);
}
if (url.pathname === "/live-status/set" && request.method === "POST") {
return handleLiveStatusSet(request, env);
}
return json({ ok: false, error: "Not found." }, 404);
},
};
+1 -1
View File
@@ -82,7 +82,7 @@
<div class="container">
<h2 class="text-center">Nebenprojekte &amp; Leidenschaften</h2>
<div class="grid grid-3">
<div class="card"><h3>🎵 Musik-Produzent</h3><p>Eigene Songs unter dem Namen „Hasidog" auf Spotify.</p></div>
<div class="card"><h3>🎵 Musik-Produzent</h3><p>Eigene Songs unter dem Namen „Hasidog" auf <a href="https://open.spotify.com/intl-de/artist/2INiE1VVJGUQdEURsltuJU" target="_blank" rel="noopener">Spotify</a>.</p></div>
<div class="card"><h3>🧸 Merchandise-Creator</h3><p>Tassen, Decken, Popsockets, Schlüsselanhänger, Sticker.</p></div>
<div class="card"><h3>📖 Autor</h3><p>Ein Buch über Hasidog &amp; Casper.</p></div>
</div>
+2 -2
View File
@@ -41,7 +41,7 @@
</div>
<div>
<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>
<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 <a href="https://open.spotify.com/intl-de/artist/2INiE1VVJGUQdEURsltuJU" target="_blank" rel="noopener">Spotify</a>.</p>
<div class="btn-row">
<a class="btn btn-primary" href="casper.html">Zur Casper-Seite 🐾</a>
<a class="btn btn-outline" href="community.html">Zur Community</a>
@@ -54,7 +54,7 @@
<div class="container">
<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>🎵 Musik</h3><p>Songs unter dem Namen „Hasidog" auf <a href="https://open.spotify.com/intl-de/artist/2INiE1VVJGUQdEURsltuJU" target="_blank" rel="noopener">Spotify</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 &amp; Casper" — die Geschichte in Buchform.</p></div>
</div>
+24 -1
View File
@@ -34,7 +34,7 @@
<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>
<span class="badge rank-1"><span class="live-dot" id="live-dot"></span><span id="live-text">Aktuell offline</span></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>
@@ -167,7 +167,30 @@
<script src="assets/js/data-aktionen.js"></script>
<script src="assets/js/main.js"></script>
<script>
// Gleiche Worker-URL wie in assets/js/forms.js / postfach.html eintragen,
// sobald der Cloudflare Worker deployed ist.
const API_BASE_URL = "";
document.addEventListener("DOMContentLoaded", () => {
// Live-Status: grüner Punkt, wenn Dogfather gerade live ist
const liveDot = document.getElementById("live-dot");
const liveText = document.getElementById("live-text");
if (API_BASE_URL && liveDot && liveText) {
fetch(`${API_BASE_URL}/live-status`)
.then((res) => res.json())
.then((data) => {
if (data && data.live) {
liveDot.classList.add("is-live");
liveText.textContent = "Live jetzt!";
} else {
liveText.textContent = "Aktuell offline";
}
})
.catch(() => {
liveText.textContent = "Aktuell offline";
});
}
// Stats: Team-Größe aus den Datenobjekten berechnen
const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length;
const statTeam = document.getElementById("stat-team");
+5 -5
View File
@@ -41,13 +41,13 @@
<h3>🎥 TikTok</h3>
<p>@dogfather0804</p>
</a>
<a class="card text-center" href="#" target="_blank" rel="noopener">
<a class="card text-center" href="https://open.spotify.com/intl-de/artist/2INiE1VVJGUQdEURsltuJU" target="_blank" rel="noopener">
<h3>🎵 Spotify — Hasidog</h3>
<p class="small muted">TODO: Spotify-Link ergänzen</p>
<p>Zum Artist-Profil</p>
</a>
<a class="card text-center" href="#" target="_blank" rel="noopener">
<a class="card text-center" href="https://open.spotify.com/intl-de/artist/4J9PcKZBkh6V8dVY3CGliw" target="_blank" rel="noopener">
<h3>🎵 Spotify — Van-Van</h3>
<p class="small muted">TODO: Spotify-Link ergänzen</p>
<p>Zum Artist-Profil</p>
</a>
<a class="card text-center" href="#" target="_blank" rel="noopener">
<h3>💬 Discord</h3>
@@ -83,7 +83,7 @@
</div>
</div>
<div class="todo-note">
<strong>TODO:</strong> Vorhandenen QR-Code <code>scout-qr-1785422377936.png</code> nach <code>assets/qr/scout-bewerbung-qr.png</code> kopieren, sobald die Datei bereitsteht. Restliche Links (Spotify, Discord, Shop) mit echten URLs befüllen.
<strong>TODO:</strong> Vorhandenen QR-Code <code>scout-qr-1785422377936.png</code> nach <code>assets/qr/scout-bewerbung-qr.png</code> kopieren, sobald die Datei bereitsteht. Restliche Links (Discord, Shop) mit echten URLs befüllen — Spotify ist bereits verlinkt.
</div>
</div>
</section>
+56
View File
@@ -64,6 +64,21 @@
</div>
</section>
<!-- ===== Live-Status umschalten (NUR für Dogi/Owner sichtbar) ===== -->
<section class="section-tight" id="live-toggle-section" style="display:none;">
<div class="container text-center">
<h2>Live-Status</h2>
<p class="lead" style="max-width:50ch;margin:0 auto;">Steuert den grünen Punkt auf der Startseite.</p>
<div class="card" style="max-width:360px;margin:1rem auto 0;">
<span class="badge rank-1"><span class="live-dot" id="admin-live-dot"></span><span id="admin-live-text">Aktuell offline</span></span>
<div class="btn-row" style="justify-content:center;margin-top:1rem;">
<button class="btn btn-primary" id="go-live-btn" type="button">🟢 Live schalten</button>
<button class="btn btn-outline" id="go-offline-btn" type="button">Offline schalten</button>
</div>
</div>
</div>
</section>
<!-- ===== Rollen-Verwaltung (NUR für Dogi/Owner sichtbar) ===== -->
<section id="admin-section" style="display:none;">
<div class="container">
@@ -122,6 +137,11 @@ document.addEventListener("DOMContentLoaded", () => {
const roleCreateStatus = document.getElementById("role-create-status");
const newCodeReveal = document.getElementById("new-code-reveal");
const rolesList = document.getElementById("roles-list");
const liveToggleSection = document.getElementById("live-toggle-section");
const adminLiveDot = document.getElementById("admin-live-dot");
const adminLiveText = document.getElementById("admin-live-text");
const goLiveBtn = document.getElementById("go-live-btn");
const goOfflineBtn = document.getElementById("go-offline-btn");
if (!API_BASE_URL) {
notConfigured.style.display = "block";
@@ -178,9 +198,12 @@ document.addEventListener("DOMContentLoaded", () => {
if (data.isOwner) {
adminSection.style.display = "block";
liveToggleSection.style.display = "block";
loadRoles();
loadLiveStatus();
} else {
adminSection.style.display = "none";
liveToggleSection.style.display = "none";
}
return true;
} catch {
@@ -234,6 +257,38 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
function renderLiveStatus(live) {
adminLiveDot.classList.toggle("is-live", !!live);
adminLiveText.textContent = live ? "Live jetzt!" : "Aktuell offline";
}
async function loadLiveStatus() {
try {
const res = await fetch(`${API_BASE_URL}/live-status`);
const data = await res.json();
if (res.ok && data.ok) renderLiveStatus(data.live);
} catch {
// still, keine Blockade der restlichen Seite
}
}
async function setLiveStatus(live) {
try {
const res = await fetch(`${API_BASE_URL}/live-status/set`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ownerCode: session.code, live }),
});
const data = await res.json();
if (res.ok && data.ok) renderLiveStatus(data.live);
} catch {
// keine Blockade
}
}
goLiveBtn.addEventListener("click", () => setLiveStatus(true));
goOfflineBtn.addEventListener("click", () => setLiveStatus(false));
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (!API_BASE_URL) {
@@ -255,6 +310,7 @@ document.addEventListener("DOMContentLoaded", () => {
sessionBar.style.display = "none";
inboxSection.style.display = "none";
adminSection.style.display = "none";
liveToggleSection.style.display = "none";
status.textContent = "";
status.className = "form-status";
});
+2 -2
View File
@@ -42,7 +42,7 @@
<div>
<h2>Wer ist VanVan?</h2>
<p>VanVan ist die rechte Hand von Dogfather — und seine beste Freundin. Sie bekommt täglich die Neuigkeiten, Aufgaben und Infos direkt von ihm und verteilt bzw. delegiert sie im Team, oder organisiert gemeinsam mit Diene.</p>
<p>Neben ihrer Rolle im Team ist VanVan auch Musik-Produzentin — ihre Songs erscheinen unter dem Namen <strong>Van-Van</strong> auf Spotify.</p>
<p>Neben ihrer Rolle im Team ist VanVan auch Musik-Produzentin — ihre Songs erscheinen unter dem Namen <strong>Van-Van</strong> auf <a href="https://open.spotify.com/intl-de/artist/4J9PcKZBkh6V8dVY3CGliw" target="_blank" rel="noopener">Spotify</a>.</p>
</div>
</div>
</section>
@@ -53,7 +53,7 @@
<div class="grid grid-3">
<div class="card text-center"><h3>🩵 Vertrauen</h3><p>Die engste Vertraute von Dogfather im Team.</p></div>
<div class="card text-center"><h3>🗂️ Organisation</h3><p>Behält den Überblick und delegiert an das richtige Teammitglied.</p></div>
<div class="card text-center"><h3>🎵 Musik</h3><p>Produziert eigene Songs als „Van-Van" auf Spotify.</p></div>
<div class="card text-center"><h3>🎵 Musik</h3><p>Produziert eigene Songs als „Van-Van" auf <a href="https://open.spotify.com/intl-de/artist/4J9PcKZBkh6V8dVY3CGliw" target="_blank" rel="noopener">Spotify</a>.</p></div>
</div>
<div class="todo-note">
<strong>TODO:</strong> Persönliches Zitat, Story und Social-Links von VanVan noch einholen (siehe Masterplan-Fragenkatalog, Block Team).