feat: plakat redesign phase 4 (chat window, contact, footer, cleanup)

- AI chat moves into a window/sheet, opened from a new speech-bubble card on
  the desk; it now respects the chatbot consent and asks before sending
  anything to n8n (previously it ignored the consent)
- Contact as red poster section with big mail address and the form on a paper
  card; validation errors are finally shown inline (aria-invalid, role=alert)
- Footer as ink strip with dk0 wordmark, legal links and a button to reopen
  the privacy settings
- "About me" in the header opens the about window on the home page
- Remove the old About section and its parts (BentoChat, ActivityFeed,
  CurrentlyReading, ReadBooks), the unused ChatWidget, Projects and BentoGrid,
  their tests, wrappers, loaders, types and texts
- scripts/directus-cleanup-redesign.mjs: dry run by default, --apply archives
  hobbies, tech stack categories and old content pages and deletes four
  unused message overrides after a full JSON backup; run only after the
  redesign is live on production

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-10-15 19:05:24 +02:00
parent 0fa10315b2
commit 6c2da49bfa
33 changed files with 534 additions and 2572 deletions
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* Räumt Directus nach dem Plakat-Redesign auf.
*
* Erst ausführen, wenn das Redesign auf production (dk0.dev) live ist — die alte
* Seite liest diese Einträge noch.
*
* node scripts/directus-cleanup-redesign.mjs # Probelauf, ändert nichts
* node scripts/directus-cleanup-redesign.mjs --apply # sichert, dann ändern
*
* Was passiert (mit --apply):
* - hobbies: alle auf status "archived" (die neue Seite zeigt keine Hobby-Karten)
* - tech_stack_categories: alle auf "archived" (Tech-Stack steht jetzt im Laufband)
* - messages: hero.badge, hero.line1, hero.line2, about.quote.idle löschen
* (überschrieben alte Hero-Texte, werden nicht mehr gelesen)
* - content_pages: home-hero und home-about (beide Sprachen) auf "archived"
*
* Vorher wird alles Betroffene vollständig nach directus-backup-redesign-<zeit>.json gesichert.
* Archivieren lässt sich im Directus-Admin jederzeit rückgängig machen.
*/
import fs from "node:fs";
import path from "node:path";
const APPLY = process.argv.includes("--apply");
function loadEnv(file) {
if (!fs.existsSync(file)) return {};
return Object.fromEntries(
fs
.readFileSync(file, "utf8")
.split("\n")
.filter((l) => /^[A-Z_]+=/.test(l))
.map((l) => {
const i = l.indexOf("=");
return [l.slice(0, i), l.slice(i + 1).replace(/^["']|["']$/g, "")];
}),
);
}
const env = { ...loadEnv(path.join(process.cwd(), ".env")), ...process.env };
const BASE = env.DIRECTUS_URL || "https://cms.dk0.dev";
const TOKEN = env.DIRECTUS_STATIC_TOKEN;
if (!TOKEN) {
console.error("DIRECTUS_STATIC_TOKEN fehlt (.env oder Umgebung).");
process.exit(1);
}
const MESSAGE_KEYS = ["hero.badge", "hero.line1", "hero.line2", "about.quote.idle"];
const PAGE_SLUGS = ["home-hero", "home-hero-de-de", "home-about", "home-about-de-de"];
async function rest(method, p, body) {
const res = await fetch(BASE + p, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`${method} ${p}: HTTP ${res.status} ${await res.text()}`);
return res.status === 204 ? null : res.json();
}
async function gql(query) {
const res = await fetch(BASE + "/graphql", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const json = await res.json();
if (json.errors) throw new Error(JSON.stringify(json.errors));
return json.data;
}
const hobbies = (await rest("GET", "/items/hobbies?fields=*,translations.*&limit=-1")).data;
const categories = (await rest("GET", "/items/tech_stack_categories?fields=*,translations.*&limit=-1")).data;
const messages = (await rest("GET", `/items/messages?fields=*,translations.*&limit=-1`)).data.filter((m) =>
MESSAGE_KEYS.includes(m.key),
);
const pages = (
await gql(`{ content_pages(limit: -1) { slug locale status title content meta_description keywords } }`)
).content_pages.filter((p) => PAGE_SLUGS.includes(p.slug));
const activeHobbies = hobbies.filter((h) => h.status !== "archived");
const activeCategories = categories.filter((c) => c.status !== "archived");
const activePages = pages.filter((p) => p.status !== "archived");
console.log(`${APPLY ? "AUSFÜHREN" : "PROBELAUF"} gegen ${BASE}\n`);
console.log(`hobbies archivieren: ${activeHobbies.length} (${activeHobbies.map((h) => h.key).join(", ") || "-"})`);
console.log(`tech_stack_categories archivieren: ${activeCategories.length} (${activeCategories.map((c) => c.key).join(", ") || "-"})`);
console.log(`messages löschen: ${messages.length} (${messages.map((m) => m.key).join(", ") || "-"})`);
console.log(`content_pages archivieren: ${activePages.length} (${activePages.map((p) => p.slug).join(", ") || "-"})`);
if (!APPLY) {
console.log("\nNichts geändert. Mit --apply ausführen, wenn das Redesign auf production live ist.");
process.exit(0);
}
const backupFile = path.join(process.cwd(), `directus-backup-redesign-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
fs.writeFileSync(backupFile, JSON.stringify({ hobbies, categories, messages, pages }, null, 2));
console.log(`\nSicherung: ${backupFile}`);
for (const h of activeHobbies) await rest("PATCH", `/items/hobbies/${h.id}`, { status: "archived" });
for (const c of activeCategories) await rest("PATCH", `/items/tech_stack_categories/${c.id}`, { status: "archived" });
for (const m of messages) await rest("DELETE", `/items/messages/${m.id}`);
for (const p of activePages) {
try {
await rest("PATCH", `/items/content_pages/${encodeURIComponent(p.slug)}`, { status: "archived" });
} catch {
// REST ist für content_pages teils gesperrt, GraphQL geht
await gql(`mutation { update_content_pages_item(id: "${p.slug}", data: { status: "archived" }) { slug } }`);
}
}
console.log("Fertig.");