feat(cms): improve admin content editing

This commit is contained in:
Schubert Ferenc 2026-07-04 11:01:29 +02:00
parent 12d31f5468
commit a284bbc188
26 changed files with 362 additions and 147 deletions

View file

@ -20,6 +20,7 @@ export const defaultContent: ContentDocuments = {
title: "Funktechnik Schubert",
subtitle: "Service, Reparatur und Diagnose für Funkgeräte, Messtechnik und Kommunikationstechnik.",
heroText: "Von CB-Funk und Amateurfunk bis zur professionellen Funktechnik: Wir unterstützen bei Fehlersuche, Abgleich, Modulationsproblemen, Frequenzproblemen und technischer Dokumentation.",
heroImage: "/workbench-signal.svg",
intro: "Von der ersten Fehlerbeschreibung bis zur dokumentierten Prüfung: Funktechnik Schubert verbindet technische Erfahrung, geeignete Messmittel und strukturierte Kommunikation.",
paragraphs: [],
cta: {
@ -62,6 +63,7 @@ export const defaultContent: ContentDocuments = {
title: "Service für Funkgeräte und Kommunikationstechnik",
subtitle: "Technische Unterstützung für Geräte, bei denen Diagnose, Messtechnik, Erfahrung und saubere Dokumentation zählen.",
heroText: "Leistungen für private und professionelle Funktechnik mit klarem Fokus auf Diagnose, Abgleich und Reparatur.",
heroImage: "/workbench-signal.svg",
intro: "Die Leistungen können im Adminbereich sortiert, erweitert und ein- oder ausgeblendet werden.",
paragraphs: [],
cta: { text: "Reparaturfall vorbereiten", primary: { label: "Reparatur anfragen", href: "/reparatur" }, secondary: { label: "Kontakt aufnehmen", href: "/kontakt" } },
@ -83,6 +85,7 @@ export const defaultContent: ContentDocuments = {
title: "Prüfung, Diagnose und Abgleich",
subtitle: "Funkgeräte zeigen Fehler oft erst im Zusammenspiel von Empfang, Sendeteil, Versorgung, Antennenanpassung, Bedienung und Abgleich.",
heroText: "Der Service betrachtet diese Zusammenhänge strukturiert und dokumentiert die technischen Schritte nachvollziehbar.",
heroImage: "/workbench-signal.svg",
intro: "Eine gute Reparatur beginnt mit vollständigen Angaben zu Gerät, Fehlerbild und Vorgeschichte.",
paragraphs: [],
cta: { text: "Reparaturprozess starten", primary: { label: "Reparatur anfragen", href: "/reparatur" }, secondary: { label: "Kontakt", href: "/kontakt" } },
@ -105,11 +108,35 @@ export const defaultContent: ContentDocuments = {
seo: seo("Funkgeräte-Service", "Werkstattservice für CB-Funk, Amateurfunk und Funkgeräte-Abgleich.", "/funkgeraete-service"),
updatedAt: now,
},
repair: {
eyebrow: "Reparaturannahme",
title: "Reparatur anfragen",
subtitle: "Beschreiben Sie Gerät, Fehlerbild, Zubehör und bisherige Vorarbeiten.",
heroText: "Die Anfrage wird strukturiert erfasst und für eine spätere technische Bearbeitung vorbereitet.",
heroImage: "/workbench-signal.svg",
intro: "Bitte senden Sie Geräte erst nach Rückmeldung ein. Vollständige Gerätedaten und eine präzise Fehlerbeschreibung helfen, den Aufwand besser einzuschätzen.",
paragraphs: [
{ title: "Vor der Einsendung", text: "Bitte senden Sie Geräte erst nach Rückmeldung ein. Vollständige Gerätedaten und eine präzise Fehlerbeschreibung helfen, den Aufwand besser einzuschätzen." },
],
cta: { text: "Reparatur vorbereiten", primary: { label: "Formular ausfüllen", href: "/reparatur" }, secondary: { label: "Kontakt", href: "/kontakt" } },
listTitle: "Wichtige Angaben",
list: [
"Hersteller, Modell, Geräteart und Seriennummer bereithalten, soweit vorhanden",
"Fehler möglichst konkret beschreiben: Empfang, Sendung, Modulation, Frequenz, Anzeige oder Versorgung",
"Zubehör wie Netzteil, Mikrofon, Antennenadapter oder Kabel angeben",
"Bereits geöffnete Geräte und durchgeführte Arbeiten ehrlich nennen",
"Fotos können später nach Rückmeldung ergänzt werden",
],
faq: [],
seo: seo("Reparaturannahme", "Reparaturanfrage für Funkgeräte strukturiert vorbereiten.", "/reparatur"),
updatedAt: now,
},
about: {
eyebrow: "Über uns",
title: "Technik verstehen, Fehler nachvollziehbar lösen",
subtitle: "Funktechnik Schubert richtet sich an Kunden, die bei Funkgeräten und Kommunikationstechnik eine persönliche, technische Einschätzung suchen.",
heroText: "Im Mittelpunkt stehen saubere Diagnose, transparente Kommunikation und respektvoller Umgang mit bestehenden Geräten.",
heroImage: "/workbench-signal.svg",
intro: "Technischer Service lebt von Genauigkeit, Erfahrung und klarer Rückmeldung.",
paragraphs: [
{ title: "Arbeitsweise", text: "Im Mittelpunkt stehen saubere Diagnose, transparente Kommunikation und der respektvolle Umgang mit bestehenden Geräten und Serviceunterlagen." },
@ -130,6 +157,7 @@ export const defaultContent: ContentDocuments = {
title: "Kontakt aufnehmen",
subtitle: "Beschreiben Sie kurz Ihr Anliegen. Für Reparaturen nutzen Sie idealerweise die strukturierte Reparaturannahme.",
heroText: "Je genauer Gerät, Anliegen und gewünschte Unterstützung beschrieben werden, desto gezielter kann die Rückmeldung erfolgen.",
heroImage: "/workbench-signal.svg",
intro: "Direkt und technisch: Nutzen Sie das Kontaktformular oder die Reparaturannahme.",
paragraphs: [],
cta: { text: "Anfrage senden", primary: { label: "Reparatur anfragen", href: "/reparatur" }, secondary: { label: "Leistungen", href: "/leistungen" } },
@ -170,6 +198,7 @@ export const contentFileNames = {
home: "home.json",
services: "services.json",
"radio-service": "radio-service.json",
repair: "repair.json",
about: "about.json",
contact: "contact.json",
settings: "settings.json",

View file

@ -4,11 +4,11 @@ import path from "path";
import { storageDirectory } from "@/lib/runtime/config";
import { listMediaFiles } from "@/lib/admin/media";
import { contentFileNames, defaultContent } from "./defaults";
import type { ContentDocuments, ContentKey, ContentSummary } from "./types";
import type { ContentBackup, ContentDocuments, ContentKey, ContentSummary } from "./types";
export const contentDirectory = path.join(storageDirectory, "content");
export const contentBackupDirectory = path.join(contentDirectory, "backups");
export const contentVersion = "0.4.0";
export const contentVersion = "0.4.1";
const contentKeys = Object.keys(contentFileNames) as ContentKey[];
@ -51,7 +51,7 @@ function filePathFor(key: ContentKey) {
function backupStamp() {
const date = new Date();
const pad = (value: number) => String(value).padStart(2, "0");
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}
async function pruneBackups(key: ContentKey) {
@ -144,10 +144,33 @@ export async function getContentSummary(): Promise<ContentSummary> {
};
}
export async function listContentBackups(limit = 20): Promise<ContentBackup[]> {
await ensureContentStorage();
const entries = await readdir(contentBackupDirectory).catch(() => []);
const backups = await Promise.all(entries
.filter((entry) => entry.endsWith(".json"))
.map(async (entry) => {
const page = entry.split(".")[0] as ContentKey;
const fullPath = path.join(contentBackupDirectory, entry);
const fileStat = await stat(fullPath);
return {
page,
fileName: entry,
createdAt: fileStat.mtime.toISOString(),
};
}));
return backups
.filter((backup) => contentKeys.includes(backup.page))
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
.slice(0, limit);
}
export async function getContentSystemStatus() {
const [summary, media] = await Promise.all([getContentSummary(), listMediaFiles()]);
const [summary, media, backups] = await Promise.all([getContentSummary(), listMediaFiles(), listContentBackups()]);
return {
...summary,
mediaCount: media.length,
backups,
};
}

View file

@ -36,6 +36,7 @@ export type PageContent = {
title: string;
subtitle: string;
heroText: string;
heroImage: string;
intro: string;
paragraphs: TextBlockContent[];
cta: {
@ -92,6 +93,8 @@ export type ContactContent = PageContent & {
socialMedia: LinkContent[];
};
export type RepairContent = PageContent;
export type SettingsContent = {
siteName: string;
logoUrl: string;
@ -108,6 +111,7 @@ export type ContentDocuments = {
home: HomeContent;
services: ServicesContent;
"radio-service": RadioServiceContent;
repair: RepairContent;
about: AboutContent;
contact: ContactContent;
settings: SettingsContent;
@ -126,3 +130,9 @@ export type ContentSummary = {
updatedAt: string;
}>;
};
export type ContentBackup = {
page: ContentKey;
fileName: string;
createdAt: string;
};

View file

@ -1 +1 @@
export const appVersion = "0.4.0";
export const appVersion = "0.4.1";