Files
Goldebek/scripts/new-trip-report.mjs
Per Hoener 0d45331f5c
Deploy / validate-dispatch (push) Successful in 43s
Deploy / scheduled-rebuild-dev (push) Skipped
Deploy / scheduled-rebuild-master (push) Skipped
Deploy / rollback-dev (push) Skipped
Deploy / rollback-master (push) Skipped
Deploy / changes (push) Successful in 13s
Deploy / secret-scan (push) Successful in 9s
Deploy / app-quality (push) Successful in 11m21s
Deploy / security-deps (push) Successful in 14s
Deploy / context (push) Failing after 1s
Deploy / app-image (push) Skipped
Deploy / deploy-dev (push) Skipped
Deploy / deploy-master (push) Skipped
Initial commit
2026-09-18 23:37:19 +02:00

172 lines
6.8 KiB
JavaScript

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) =>
` <TripImage src=${JSON.stringify(image.publicPath)} alt=${JSON.stringify(image.alt)} caption=${JSON.stringify(image.caption)} />`,
);
const imports = images.length ? 'import TripImage from "@/components/TripImage";\n' : "";
const body = [" <p>Berichtstext hier schreiben.</p>", ...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();
}