feat: plakat redesign phase 4 (chat window, contact, footer, cleanup) (#77)
- 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:
@@ -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.");
|
||||
Reference in New Issue
Block a user