Initial commit: FL-Akademie LMS mit Docker, Admin, Portal und Dokumentation.

Made-with: Cursor
This commit is contained in:
lo
2026-04-13 23:17:07 +02:00
commit d3367f0046
66 changed files with 3641 additions and 0 deletions

95
lib/landing.ts Normal file
View File

@@ -0,0 +1,95 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
export type LandingContentV1 = {
version: 1;
heroTitle: string;
heroLead: string;
primaryCta: { label: string; href: string };
secondaryCta?: { label: string; href: string };
benefitSectionTitle: string;
benefits: { title: string; body: string }[];
};
export const defaultLandingContent = (): LandingContentV1 => ({
version: 1,
heroTitle: "Motorrad fahren von AZ",
heroLead:
"Von der Fahrschule zur ersten Serpentine. Tipps, Übungen und Kurse strukturiert auf einer Plattform.",
primaryCta: { label: "Zu den Kursen", href: "/kurse" },
secondaryCta: { label: "Mitgliederbereich", href: "/portal" },
benefitSectionTitle: "Deine Vorteile",
benefits: [
{
title: "Praxisorientiert",
body: "Inhalte, die du auf den Platz und in den Alltag übernehmen kannst Schritt für Schritt.",
},
{
title: "Von A bis Z",
body: "Klare Module statt Wildwuchs: vom ersten Gedanken ans Motorradfahren bis zu gezielten Übungen.",
},
{
title: "Fortschritt & Zertifikat",
body: "Behalte deinen Lernstand im Blick und sichere dir nach Abschluss eine Teilnahmebestätigung.",
},
],
});
function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null;
}
export function parseLandingContent(raw: unknown): LandingContentV1 {
if (!isRecord(raw) || raw.version !== 1) return defaultLandingContent();
const heroTitle = typeof raw.heroTitle === "string" ? raw.heroTitle : "";
const heroLead = typeof raw.heroLead === "string" ? raw.heroLead : "";
const benefitSectionTitle =
typeof raw.benefitSectionTitle === "string" ? raw.benefitSectionTitle : "Deine Vorteile";
const primaryCta = isRecord(raw.primaryCta)
? {
label: typeof raw.primaryCta.label === "string" ? raw.primaryCta.label : "Mehr",
href: typeof raw.primaryCta.href === "string" ? raw.primaryCta.href : "/kurse",
}
: { label: "Zu den Kursen", href: "/kurse" };
let secondaryCta: LandingContentV1["secondaryCta"];
if (isRecord(raw.secondaryCta)) {
const l = typeof raw.secondaryCta.label === "string" ? raw.secondaryCta.label : "";
const h = typeof raw.secondaryCta.href === "string" ? raw.secondaryCta.href : "";
if (l && h) secondaryCta = { label: l, href: h };
}
const benefitsRaw = Array.isArray(raw.benefits) ? raw.benefits : [];
const benefits = benefitsRaw
.map((b) => {
if (!isRecord(b)) return null;
const title = typeof b.title === "string" ? b.title : "";
const body = typeof b.body === "string" ? b.body : "";
if (!title && !body) return null;
return { title, body };
})
.filter(Boolean) as { title: string; body: string }[];
return {
version: 1,
heroTitle: heroTitle || defaultLandingContent().heroTitle,
heroLead: heroLead || defaultLandingContent().heroLead,
primaryCta,
secondaryCta,
benefitSectionTitle,
benefits: benefits.length ? benefits : defaultLandingContent().benefits,
};
}
export async function getLandingContent(): Promise<LandingContentV1> {
const row = await prisma.landingPage.findUnique({ where: { id: "default" } });
if (!row) return defaultLandingContent();
return parseLandingContent(row.content);
}
export async function saveLandingContent(content: LandingContentV1) {
const json = content as unknown as Prisma.InputJsonValue;
await prisma.landingPage.upsert({
where: { id: "default" },
create: { id: "default", content: json },
update: { content: json },
});
}