Stand 16.10.2026. Enthält das Marketing-Dashboard, die Daten in daten/, die Produktionsvorgaben, Reels und das Webinar mit Skript, Sprechzettel und Präsentation (HTML, PowerPoint, PDF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
7.5 KiB
JavaScript
209 lines
7.5 KiB
JavaScript
// Dashboard: Daten laden, Seiten wechseln, Änderungen von selbst nachladen.
|
|
//
|
|
// Die Daten liegen in daten/*.json. Ändert Claude dort etwas, merkt das
|
|
// Dashboard es über /api/stand und zeichnet die Seite neu, ohne dass jemand
|
|
// neu laden muss.
|
|
|
|
import { esc, zeitstempel } from "./hilfen.js";
|
|
import * as start from "./seiten/start.js";
|
|
import * as plan from "./seiten/plan.js";
|
|
import * as roadmap from "./seiten/roadmap.js";
|
|
import * as formate from "./seiten/formate.js";
|
|
import * as dok from "./seiten/dokumente.js";
|
|
import * as webinar from "./seiten/webinar.js";
|
|
|
|
const SEITEN = { start, plan, roadmap, formate, webinar, dok };
|
|
|
|
export const S = {
|
|
plan: null, roadmap: null, kanaele: null, webinar: null, dokumente: [], stand: null,
|
|
slots: [], schritte: [],
|
|
};
|
|
|
|
async function holeJson(url) {
|
|
const r = await fetch(url, { cache: "no-store" });
|
|
if (!r.ok) throw new Error(`${url}: ${r.status}`);
|
|
return r.json();
|
|
}
|
|
|
|
export async function laden() {
|
|
const [p, r, k, w, d, st] = await Promise.all([
|
|
holeJson("/daten/redaktionsplan.json"),
|
|
holeJson("/daten/roadmap.json"),
|
|
holeJson("/daten/kanaele.json"),
|
|
holeJson("/daten/webinar.json").catch(() => null),
|
|
holeJson("/api/dokumente"),
|
|
holeJson("/api/stand"),
|
|
]);
|
|
Object.assign(S, { plan: p, roadmap: r, kanaele: k, webinar: w, dokumente: d, stand: st });
|
|
// Nach Datum sortiert. Am selben Tag gilt die Reihenfolge in der Datei.
|
|
S.slots = p.slots.map((s, i) => [s, i])
|
|
.sort(([a, i], [b, j]) => a.datum.localeCompare(b.datum) || i - j)
|
|
.map(([s]) => s);
|
|
S.schritte = r.pakete.flatMap((pk) => pk.schritte.map((s) => ({ ...s, paket: pk.id, paketTitel: pk.titel })));
|
|
standZeigen();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Router
|
|
|
|
export function route() {
|
|
const roh = location.hash.replace(/^#/, "") || "/";
|
|
const [pfad, abfrage] = roh.split("?");
|
|
const teile = pfad.split("/").filter(Boolean).map((t) => { try { return decodeURIComponent(t); } catch { return t; } });
|
|
const seite = SEITEN[teile[0]] ? teile[0] : "start";
|
|
return { seite, rest: teile.slice(1), query: new URLSearchParams(abfrage || "") };
|
|
}
|
|
|
|
let letzterSchluessel = null;
|
|
|
|
export async function zeichnen() {
|
|
const r = route();
|
|
const main = document.getElementById("inhalt");
|
|
// Gleiche Seite: Scrollstand behalten (zum Beispiel beim Öffnen eines Slots
|
|
// oder wenn sich Daten ändern). Andere Seite: nach oben.
|
|
const schluessel = r.seite === "dok" ? `dok/${r.rest.join("/")}?${r.query}` : r.seite;
|
|
const gleich = schluessel === letzterSchluessel;
|
|
const y = window.scrollY;
|
|
|
|
for (const a of document.querySelectorAll("#navigation a")) {
|
|
a.classList.toggle("aktiv", a.dataset.seite === r.seite);
|
|
}
|
|
schubladeWeg();
|
|
// Erst nach oben, dann zeichnen: so darf die Seite selbst noch zu einer
|
|
// Stelle springen (laufende Woche, Abschnitt, Suchtreffer).
|
|
if (!gleich) window.scrollTo(0, 0);
|
|
letzterSchluessel = schluessel;
|
|
try {
|
|
await SEITEN[r.seite].zeichnen(main, r, { erstesMal: !gleich });
|
|
} catch (e) {
|
|
console.error(e);
|
|
main.innerHTML = `<section class="block warnung"><h2>Diese Seite ließ sich nicht zeichnen</h2><p>${esc(e.message)}</p></section>`;
|
|
}
|
|
if (gleich) window.scrollTo(0, y);
|
|
document.title = `${document.querySelector("#navigation a.aktiv span")?.textContent.trim() || "Übersicht"}, HiveGrid Marketing`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Schublade
|
|
|
|
export function schublade(html, { zurueck, breit = false } = {}) {
|
|
const el = document.getElementById("schublade");
|
|
const schleier = document.getElementById("schleier");
|
|
el.className = "schublade" + (breit ? " breit" : "");
|
|
el.innerHTML = `<button class="zu" title="Esc">Schließen</button>${html}`;
|
|
el.hidden = false;
|
|
schleier.hidden = false;
|
|
const zu = () => { location.hash = zurueck; };
|
|
el.querySelector(".zu").onclick = zu;
|
|
schleier.onclick = zu;
|
|
el.dataset.zurueck = zurueck;
|
|
return el;
|
|
}
|
|
|
|
export function schubladeWeg() {
|
|
document.getElementById("schublade").hidden = true;
|
|
document.getElementById("schleier").hidden = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Meldung
|
|
|
|
let meldungZeit = null;
|
|
export function meldung(text, fehler = false) {
|
|
const el = document.getElementById("meldung");
|
|
el.textContent = text;
|
|
el.className = "meldung" + (fehler ? " fehler" : "");
|
|
el.hidden = false;
|
|
clearTimeout(meldungZeit);
|
|
meldungZeit = setTimeout(() => { el.hidden = true; }, fehler ? 6000 : 2200);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Schreiben
|
|
|
|
export async function statusSetzen(datei, id, status) {
|
|
try {
|
|
const r = await fetch("/api/status", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ datei, id, status }),
|
|
});
|
|
const j = await r.json().catch(() => ({}));
|
|
if (!r.ok || !j.ok) throw new Error(j.fehler || j.ausgabe || `Fehler ${r.status}`);
|
|
meldung(`${id}: ${status}`);
|
|
} catch (e) {
|
|
meldung(`Nicht gespeichert: ${e.message}`, true);
|
|
}
|
|
await laden();
|
|
await zeichnen();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Stand und Nachladen
|
|
|
|
function standZeigen() {
|
|
const st = S.stand;
|
|
const hinweis = document.getElementById("stand-hinweis");
|
|
hinweis.textContent = S.plan ? `${S.plan.status} vom ${S.plan.stand.split("-").reverse().join(".")}` : "";
|
|
hinweis.title = st?.aenderung ? `Letzte Änderung im Hub: ${zeitstempel(st.aenderung)}` : "";
|
|
const links = document.getElementById("links");
|
|
if (S.kanaele && !links.childElementCount) {
|
|
links.innerHTML = S.kanaele.links.map((l) => `<a href="${esc(l.url)}" target="_blank" rel="noopener"${l.lokal ? " data-lokal" : ""}>${l.logo ? `<img src="${esc(l.logo)}" alt="">` : "<span class=\"ohne-logo\"></span>"}<span>${esc(l.name)}</span></a>`).join("");
|
|
lokaleLinksPruefen();
|
|
}
|
|
}
|
|
|
|
// Lokale Werkzeuge wie PostGrid laufen nur, wenn ihr Server gestartet ist.
|
|
// Ist er aus, wird der Link blass und sagt das, statt ins Leere zu führen.
|
|
async function lokaleLinksPruefen() {
|
|
for (const a of document.querySelectorAll("#links a[data-lokal]")) {
|
|
let da = true;
|
|
try {
|
|
await fetch(a.href, { method: "HEAD", mode: "no-cors", cache: "no-store" });
|
|
} catch {
|
|
da = false;
|
|
}
|
|
a.classList.toggle("aus", !da);
|
|
a.title = da ? "" : "Läuft gerade nicht. In posts ./serve.sh starten.";
|
|
}
|
|
}
|
|
|
|
let getrennt = false;
|
|
async function nachsehen() {
|
|
let st;
|
|
try {
|
|
st = await holeJson("/api/stand");
|
|
} catch {
|
|
if (!getrennt) meldung("Server nicht erreichbar. Läuft ./serve.sh noch?", true);
|
|
getrennt = true;
|
|
return;
|
|
}
|
|
getrennt = false;
|
|
if (S.stand && st.aenderung !== S.stand.aenderung) {
|
|
await laden();
|
|
// Nicht neu zeichnen, solange jemand in ein Feld tippt.
|
|
if (!document.activeElement?.matches("input, textarea")) {
|
|
await zeichnen();
|
|
meldung("Aktualisiert");
|
|
}
|
|
} else {
|
|
S.stand = st;
|
|
standZeigen();
|
|
}
|
|
}
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Escape") return;
|
|
const el = document.getElementById("schublade");
|
|
if (!el.hidden && el.dataset.zurueck) location.hash = el.dataset.zurueck;
|
|
});
|
|
|
|
window.addEventListener("hashchange", zeichnen);
|
|
|
|
(async () => {
|
|
try {
|
|
await laden();
|
|
} catch (e) {
|
|
document.getElementById("inhalt").innerHTML =
|
|
`<section class="block warnung"><h2>Die Daten ließen sich nicht laden</h2><p>${esc(e.message)}</p><p>Läuft der Server? Im Hub-Ordner <code>./serve.sh</code> starten.</p></section>`;
|
|
return;
|
|
}
|
|
await zeichnen();
|
|
setInterval(nachsehen, 4000);
|
|
setInterval(lokaleLinksPruefen, 15000);
|
|
})();
|