import { getDictionary } from "@/i18n/get-dictionary"; import { siteConfig } from "@/lib/site-config"; import { DEFAULT_SHIPPING_REGION } from "@/lib/shipping-region"; import type { Locale } from "@/i18n/config"; import type { TitledText, LegalSectionContent } from "@/i18n/dictionary-type"; export type ContentFieldType = | "text" | "textarea" /** Feste Auswahl statt freiem Text — siehe "options" an ContentFieldDef. */ | "select" | "list-simple" | "list-titled" | "list-numbered" | "richtext-sections" | "image" | "video"; export interface ContentFieldDef { key: string; group: "brand" | "company" | "shipping" | "home" | "pages" | "legal" | "social"; labelDe: string; labelEn: string; type: ContentFieldType; /** false = ein einziger Wert unabhaengig von der Locale (Firmendaten). */ localized: boolean; /** Liefert den aktuellen Fallback-Text im selben Format, das der Parser fuer diesen Typ erwartet. */ getDefault: (locale: Locale) => string; /** * Nur bei type: "select" — die waehlbaren Werte. Ein freies Textfeld waere * hier falsch: ein Tippfehler im Liefergebiet wuerde stillschweigend auf * den Vorgabewert zurueckfallen, und niemand saehe, dass die Umstellung * nicht gegriffen hat. */ options?: { value: string; labelDe: string; labelEn: string }[]; } function joinSimpleList(items: string[]): string { return items.join("\n"); } function joinTitledList(items: TitledText[]): string { return items.map((i) => `${i.title} | ${i.text}`).join("\n"); } function joinNumberedList(items: { number: string; title: string; text: string }[]): string { return items.map((i) => `${i.number} | ${i.title} | ${i.text}`).join("\n"); } function joinSections(sections: LegalSectionContent[]): string { return sections .map((s) => `## ${s.heading}\n\n${s.paragraphs.join("\n\n")}`) .join("\n\n"); } export const CONTENT_REGISTRY: ContentFieldDef[] = [ // --- Logo und Favicon (nicht lokalisiert). Leer = das gezeichnete // Waben-Zeichen (components/logomark.tsx) bzw. das Standard-Icon // (lib/brand-icon.ts). Nur Rasterbilder: der Upload nimmt bewusst kein SVG // an (ein SVG kann Skripte enthalten). Gelesen ueber lib/brand.ts. { key: "brand.logo", group: "brand", labelDe: "Logo (PNG mit transparentem Hintergrund, erscheint neben dem Firmennamen)", labelEn: "Logo (PNG with transparent background, shown next to the company name)", type: "image", localized: false, getDefault: () => "" }, { key: "brand.logoInverted", group: "brand", labelDe: "Logo für dunkle Flächen (optional: Mobilmenü, Fußzeile, Admin-Leiste; leer = normales Logo)", labelEn: "Logo for dark backgrounds (optional: mobile menu, footer, admin sidebar; empty = regular logo)", type: "image", localized: false, getDefault: () => "" }, { key: "brand.favicon", group: "brand", labelDe: "Favicon (quadratisch, mindestens 512 × 512 px; leer = Standard-Wabe)", labelEn: "Favicon (square, at least 512 × 512 px; empty = default honeycomb)", type: "image", localized: false, getDefault: () => "" }, // --- Firmendaten (nicht lokalisiert) --- { key: "company.name", group: "company", labelDe: "Firmenname", labelEn: "Company name", type: "text", localized: false, getDefault: () => siteConfig.name }, { key: "company.claim", group: "company", labelDe: "Slogan", labelEn: "Tagline", type: "text", localized: false, getDefault: () => siteConfig.claim }, { key: "company.claimAlt", group: "company", labelDe: "Alternativer Slogan", labelEn: "Alternate tagline", type: "text", localized: false, getDefault: () => siteConfig.claimAlt }, { key: "company.description", group: "company", labelDe: "Kurzbeschreibung", labelEn: "Short description", type: "textarea", localized: false, getDefault: () => siteConfig.description }, { key: "company.location", group: "company", labelDe: "Standort (Anzeigetext)", labelEn: "Location (display text)", type: "text", localized: false, getDefault: () => siteConfig.location }, { key: "company.email", group: "company", labelDe: "E-Mail", labelEn: "Email", type: "text", localized: false, getDefault: () => siteConfig.email }, { key: "company.phone", group: "company", labelDe: "Telefon", labelEn: "Phone", type: "text", localized: false, getDefault: () => siteConfig.phone }, { key: "company.vatId", group: "company", labelDe: "Umsatzsteuer-Identifikationsnummer (erscheint auf jeder Rechnung; in Texten per Baustein {{USt-IdNr}} einfügbar)", labelEn: "VAT identification number (shown on every invoice; insert into texts with the placeholder {{USt-IdNr}})", type: "text", localized: false, getDefault: () => siteConfig.vatId }, { key: "company.street", group: "company", labelDe: "Straße", labelEn: "Street", type: "text", localized: false, getDefault: () => siteConfig.address.street }, { key: "company.zip", group: "company", labelDe: "PLZ", labelEn: "ZIP", type: "text", localized: false, getDefault: () => siteConfig.address.zip }, { key: "company.city", group: "company", labelDe: "Ort", labelEn: "City", type: "text", localized: false, getDefault: () => siteConfig.address.city }, // Kopie jeder Rechnung und Stornorechnung (lib/invoice-mail.ts). Leer = keine Kopie. { key: "invoice.copyEmail", group: "company", labelDe: "Rechnungskopie an (E-Mail-Adresse; jede Rechnung und Stornorechnung geht als E-Rechnung zusätzlich an diese Adresse, leer = keine Kopie)", labelEn: "Invoice copy to (email address; every invoice and cancellation invoice is also sent as an e-invoice to this address, empty = no copy)", type: "text", localized: false, getDefault: () => "" }, // --- Liefergebiet --- // Steuert dreierlei zugleich: die Laenderauswahl im Checkout, die // verbindliche Pruefung beim Anlegen der Bestellung (api/checkout/route.ts) // und den Hinweis auf der Seite "Versand und Zahlung". Bewusst eine // Auswahl und kein Freitext — siehe "options" oben. { key: "shipping.region", group: "shipping", labelDe: "Liefergebiet", labelEn: "Shipping region", type: "select", localized: false, getDefault: () => DEFAULT_SHIPPING_REGION, options: [ { value: "DE", labelDe: "Nur Deutschland", labelEn: "Germany only" }, { value: "EU", labelDe: "Europäische Union", labelEn: "European Union" }, ], }, // --- Homepage (lokalisiert) --- { key: "home.hero.intro", group: "home", labelDe: "Hero: Einleitungstext", labelEn: "Hero: Intro text", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.hero.intro }, { key: "home.hero.footnote", group: "home", labelDe: "Hero: Fußnote", labelEn: "Hero: Footnote", type: "text", localized: true, getDefault: (l) => getDictionary(l).home.hero.footnote }, { key: "home.originStory.paragraph1", group: "home", labelDe: "Herkunft: Absatz 1", labelEn: "Origin: Paragraph 1", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.originStory.paragraph1 }, { key: "home.originStory.paragraph2", group: "home", labelDe: "Herkunft: Absatz 2", labelEn: "Origin: Paragraph 2", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.originStory.paragraph2 }, { key: "home.originStory.facts", group: "home", labelDe: "Herkunft: Fakten (eine Zeile je Fakt)", labelEn: "Origin: Facts (one per line)", type: "list-simple", localized: true, getDefault: (l) => joinSimpleList(getDictionary(l).home.originStory.facts) }, { key: "home.philosophy.items", group: "home", labelDe: "Philosophie: Punkte (Titel | Text, eine Zeile je Punkt)", labelEn: "Philosophy: Items (Title | Text, one per line)", type: "list-titled", localized: true, getDefault: (l) => joinTitledList(getDictionary(l).home.philosophy.items) }, { key: "home.processTimeline.steps", group: "home", labelDe: "Ablauf: Schritte (Nr. | Titel | Text, eine Zeile je Schritt)", labelEn: "Process: Steps (No. | Title | Text, one per line)", type: "list-numbered", localized: true, getDefault: (l) => joinNumberedList(getDictionary(l).home.processTimeline.steps) }, { key: "home.regionalSection.paragraph1", group: "home", labelDe: "Regional: Absatz 1", labelEn: "Regional: Paragraph 1", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.regionalSection.paragraph1 }, { key: "home.regionalSection.paragraph2", group: "home", labelDe: "Regional: Absatz 2", labelEn: "Regional: Paragraph 2", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.regionalSection.paragraph2 }, { key: "home.sustainability.items", group: "home", labelDe: "Nachhaltigkeit: Punkte (Titel | Text, eine Zeile je Punkt)", labelEn: "Sustainability: Items (Title | Text, one per line)", type: "list-titled", localized: true, getDefault: (l) => joinTitledList(getDictionary(l).home.sustainability.items) }, { key: "home.businessTeaser.paragraph", group: "home", labelDe: "Geschäftskunden-Hinweis: Text", labelEn: "Business teaser: Text", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.businessTeaser.paragraph }, // --- Homepage-Bilder (nicht lokalisiert — ein Bild gilt fuer beide // Sprachen; Wert ist der von app/api/admin/upload/route.ts zurueckgegebene // Pfad, z.B. "/uploads/.webp". Leer = kein Bild gepflegt, die // Komponente zeigt dann weiterhin den bisherigen CSS-Platzhalter, siehe // components/content-image.tsx.) Deckt jede Stelle in hero.tsx/ // home-sections-a.tsx/home-sections-b.tsx ab, die ein festes, // redaktionelles Bild zeigt — der homepage-Produkt-Spotlight-Abschnitt // (ProductSpotlight) ist bewusst NICHT hier: er zeigt kein festes Bild, // sondern das jeweils dynamisch ausgewaehlte Produkt und nutzt dafuer // dessen eigenes Product.images (siehe admin/produkte). { key: "home.hero.image", group: "home", labelDe: "Hero: Bild", labelEn: "Hero: Image", type: "image", localized: false, getDefault: () => "" }, // Optionales Hintergrundvideo fuer den Hero (MP4/WebM, siehe // app/api/admin/upload-video/route.ts). Wenn gepflegt, zeigt hero.tsx das // Video statt des Hero-Bilds/Platzhalters an derselben Stelle — home.hero.image // bleibt als Fallback/Poster-Bild erhalten, falls das Video wieder entfernt wird. { key: "home.hero.video", group: "home", labelDe: "Hero: Hintergrundvideo (MP4/WebM, optional, ersetzt das Hero-Bild)", labelEn: "Hero: Background video (MP4/WebM, optional, replaces the hero image)", type: "video", localized: false, getDefault: () => "" }, { key: "home.originStory.image", group: "home", labelDe: "Herkunft: Bild", labelEn: "Origin: Image", type: "image", localized: false, getDefault: () => "" }, { key: "home.regionalSection.image", group: "home", labelDe: "Regional: Bild", labelEn: "Regional: Image", type: "image", localized: false, getDefault: () => "" }, { key: "home.businessTeaser.image", group: "home", labelDe: "Geschäftskunden-Hinweis: Bild", labelEn: "Business teaser: Image", type: "image", localized: false, getDefault: () => "" }, // --- Weitere Seiten (nicht lokalisiert) --- { key: "imkerei.image", group: "pages", labelDe: "Imkerei-Seite: Bild", labelEn: "Imkerei page: Image", type: "image", localized: false, getDefault: () => "" }, { key: "herkunft.image", group: "pages", labelDe: "Herkunft-Seite: Bild", labelEn: "Herkunft page: Image", type: "image", localized: false, getDefault: () => "" }, { key: "unternehmen.image", group: "pages", labelDe: "Geschäftskunden-Seite: Bild", labelEn: "Business page: Image", type: "image", localized: false, getDefault: () => "" }, // --- Social Media (nicht lokalisiert, leer = Icon wird im Footer nicht // angezeigt, siehe components/site-footer.tsx Footer()) --- { key: "social.tiktok", group: "social", labelDe: "TikTok-Profil (URL)", labelEn: "TikTok profile (URL)", type: "text", localized: false, getDefault: () => "" }, { key: "social.youtube", group: "social", labelDe: "YouTube-Kanal (URL)", labelEn: "YouTube channel (URL)", type: "text", localized: false, getDefault: () => "" }, { key: "social.instagram", group: "social", labelDe: "Instagram-Profil (URL)", labelEn: "Instagram profile (URL)", type: "text", localized: false, getDefault: () => "" }, // --- Rechtstexte (lokalisiert) --- { key: "legal.impressum", group: "legal", labelDe: "Impressum", labelEn: "Legal notice", type: "richtext-sections", localized: true, getDefault: (l) => joinSections(getDictionary(l).impressumPage.sections) }, { key: "legal.datenschutz", group: "legal", labelDe: "Datenschutz", labelEn: "Privacy policy", type: "richtext-sections", localized: true, getDefault: (l) => joinSections(getDictionary(l).datenschutzPage.sections) }, { key: "legal.agb", group: "legal", labelDe: "AGB", labelEn: "Terms and conditions", type: "richtext-sections", localized: true, getDefault: (l) => joinSections(getDictionary(l).agbPage.sections) }, { key: "legal.widerruf", group: "legal", labelDe: "Widerrufsbelehrung", labelEn: "Right of withdrawal", type: "richtext-sections", localized: true, getDefault: (l) => joinSections(getDictionary(l).widerrufPage.sections) }, { key: "legal.cookieRichtlinie", group: "legal", labelDe: "Cookie-Richtlinie", labelEn: "Cookie policy", type: "richtext-sections", localized: true, getDefault: (l) => joinSections(getDictionary(l).cookieRichtliniePage.sections) }, ]; export function getContentFieldDef(key: string): ContentFieldDef | undefined { return CONTENT_REGISTRY.find((f) => f.key === key); }