import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; import { stdin as input, stdout as output } from "node:process"; import { fileURLToPath } from "node:url"; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const reportsRoot = path.join(root, "src", "trip-reports"); const registryPath = path.join(reportsRoot, "index.ts"); const imageSourceRoot = path.join(root, ".media-source", "original-content"); const supportedImageExtensions = new Set([".jpg", ".jpeg", ".png"]); const prompt = readline.createInterface({ input, output }); const lines = prompt[Symbol.asyncIterator](); function slugify(value) { return value .replace(/ß/g, "ss") .replace(/[äÄ]/g, "ae") .replace(/[öÖ]/g, "oe") .replace(/[üÜ]/g, "ue") .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function componentName(slug) { const name = slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(""); return /^\d/.test(name) ? `Report${name}` : name; } async function ask(label, fallback = "") { output.write(`${label}${fallback ? ` [${fallback}]` : ""}: `); const answer = await lines.next(); if (answer.done) throw new Error("Eingabe wurde unerwartet beendet."); return answer.value.trim() || fallback; } async function askRequired(label, fallback = "") { while (true) { const answer = await ask(label, fallback); if (answer) return answer; console.log("Bitte einen Wert eingeben."); } } async function askBoolean(label, fallback) { const answer = (await ask(`${label} (${fallback ? "J/n" : "j/N"})`)).toLowerCase(); if (!answer) return fallback; if (["j", "ja", "y", "yes"].includes(answer)) return true; if (["n", "nein", "no"].includes(answer)) return false; throw new Error("Bitte mit ja oder nein antworten."); } function parseDate(value) { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new Error("Datum muss YYYY-MM-DD entsprechen."); const date = new Date(`${value}T00:00:00.000Z`); if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) { throw new Error("Das Datum ist ungueltig."); } return date; } async function collectImages(year) { const raw = await ask("Bildpfade, kommagetrennt (optional)"); if (!raw) return []; const images = []; for (const entry of raw.split(",").map((value) => value.trim()).filter(Boolean)) { const sourcePath = path.resolve(process.cwd(), entry); if (!fs.statSync(sourcePath, { throwIfNoEntry: false })?.isFile()) { throw new Error(`Bild nicht gefunden: ${sourcePath}`); } const extension = path.extname(sourcePath).toLowerCase(); if (!supportedImageExtensions.has(extension)) throw new Error(`Nicht unterstuetztes Bildformat: ${sourcePath}`); const fileName = path.basename(sourcePath); const alt = await askRequired(`Alt-Text fuer ${fileName}`); const caption = await ask(`Bildunterschrift fuer ${fileName}`, alt); images.push({ sourcePath, destinationPath: path.join(imageSourceRoot, String(year), fileName), publicPath: `/images/content/${year}/${fileName}`, alt, caption, }); } return images; } function createReport({ slug, title, author, date, published, excerpt, images }) { const name = componentName(slug); const imageMarkup = images.map((image) => ` `, ); const imports = images.length ? 'import TripImage from "@/components/TripImage";\n' : ""; const body = ["

Berichtstext hier schreiben.

", ...imageMarkup].join("\n"); return `${imports}import type { TripReportMetadata } from "./types"; export const metadata = { slug: ${JSON.stringify(slug)}, title: ${JSON.stringify(title)}, author: ${JSON.stringify(author)}, createdAt: ${JSON.stringify(date.toISOString())}, updatedAt: ${JSON.stringify(date.toISOString())}, published: ${published}, excerpt: ${JSON.stringify(excerpt)}, plainText: ${JSON.stringify(excerpt)}, } satisfies TripReportMetadata; export default function ${name}() { return ( <> ${body} ); } `; } function registerReport(registry, slug) { const name = componentName(slug); const importMarker = 'import type { TripReportModule } from "./types";'; const listMarker = "const tripReports: readonly TripReportModule[] = ["; if (!registry.includes(importMarker) || !registry.includes(listMarker)) { throw new Error("Die Beitrags-Registry hat ein unbekanntes Format."); } const importLine = `import ${name}, { metadata as ${name.charAt(0).toLowerCase() + name.slice(1)}Metadata } from "./${slug}";\n`; const entry = `\n { metadata: ${name.charAt(0).toLowerCase() + name.slice(1)}Metadata, Component: ${name} },`; return registry .replace(importMarker, `${importLine}${importMarker}`) .replace(listMarker, `${listMarker}${entry}`); } async function main() { console.log("Neuen TSX-Beitrag anlegen\n"); const title = await askRequired("Titel"); const author = await askRequired("Autor"); const date = parseDate(await ask("Berichtsdatum (YYYY-MM-DD)", new Date().toISOString().slice(0, 10))); const year = date.getUTCFullYear(); const suggestedSlug = `${slugify(title)}-${year}`.replace(new RegExp(`-${year}-${year}$`), `-${year}`); const slug = await askRequired("Slug", suggestedSlug); if (slugify(slug) !== slug) throw new Error(`Ungueltiger Slug. Vorschlag: ${slugify(slug)}`); const excerpt = await askRequired("Kurzbeschreibung fuer Uebersicht und SEO"); const published = await askBoolean("Direkt veroeffentlichen?", true); const images = await collectImages(year); const reportPath = path.join(reportsRoot, `${slug}.tsx`); if (fs.existsSync(reportPath)) throw new Error(`Beitrag existiert bereits: ${reportPath}`); for (const image of images) { if (fs.existsSync(image.destinationPath)) throw new Error(`Zielbild existiert bereits: ${image.destinationPath}`); } const registry = fs.readFileSync(registryPath, "utf8"); const nextRegistry = registerReport(registry, slug); fs.mkdirSync(path.join(imageSourceRoot, String(year)), { recursive: true }); for (const image of images) fs.copyFileSync(image.sourcePath, image.destinationPath, fs.constants.COPYFILE_EXCL); fs.writeFileSync(reportPath, createReport({ slug, title, author, date, published, excerpt, images }), { flag: "wx" }); fs.writeFileSync(registryPath, nextRegistry); console.log(`\nBeitrag angelegt: ${path.relative(process.cwd(), reportPath)}`); console.log("Naechster Schritt: Berichtstext in der TSX-Datei ausarbeiten."); } try { await main(); } catch (error) { console.error(`\nFehler: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; } finally { prompt.close(); }