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) =>
`
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(); }