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

View File

@@ -0,0 +1,51 @@
"use server";
import { revalidatePath } from "next/cache";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth-options";
import type { LandingContentV1 } from "@/lib/landing";
import { saveLandingContent } from "@/lib/landing";
async function assertAdmin() {
const session = await getServerSession(authOptions);
if (!session?.user?.id || session.user.role !== "ADMIN") {
throw new Error("Keine Berechtigung.");
}
}
export async function updateLandingAction(formData: FormData) {
await assertAdmin();
const heroTitle = String(formData.get("heroTitle") ?? "").trim();
const heroLead = String(formData.get("heroLead") ?? "").trim();
const primaryLabel = String(formData.get("primaryLabel") ?? "").trim();
const primaryHref = String(formData.get("primaryHref") ?? "").trim();
const secondaryLabel = String(formData.get("secondaryLabel") ?? "").trim();
const secondaryHref = String(formData.get("secondaryHref") ?? "").trim();
const benefitSectionTitle = String(formData.get("benefitSectionTitle") ?? "").trim();
const benefits: { title: string; body: string }[] = [];
for (let i = 1; i <= 6; i++) {
const title = String(formData.get(`benefit${i}Title`) ?? "").trim();
const body = String(formData.get(`benefit${i}Body`) ?? "").trim();
if (title || body) benefits.push({ title, body });
}
const content: LandingContentV1 = {
version: 1,
heroTitle,
heroLead,
primaryCta: {
label: primaryLabel || "Zu den Kursen",
href: primaryHref || "/kurse",
},
secondaryCta:
secondaryLabel && secondaryHref ? { label: secondaryLabel, href: secondaryHref } : undefined,
benefitSectionTitle: benefitSectionTitle || "Deine Vorteile",
benefits,
};
await saveLandingContent(content);
revalidatePath("/");
revalidatePath("/admin/landing");
}

View File

@@ -0,0 +1,75 @@
import { getLandingContent } from "@/lib/landing";
import { updateLandingAction } from "@/app/admin/landing/actions";
export default async function AdminLandingPage() {
const c = await getLandingContent();
const benefits = [...c.benefits];
while (benefits.length < 6) benefits.push({ title: "", body: "" });
return (
<div>
<h1 className="page-title">Startseite bearbeiten</h1>
<p className="muted subtitle">
Änderungen sind nach dem Speichern sofort auf der öffentlichen Startseite sichtbar.
</p>
<form action={updateLandingAction} className="panel form" style={{ maxWidth: 820 }}>
<label>
Hero-Titel
<input name="heroTitle" defaultValue={c.heroTitle} required />
</label>
<label>
Hero-Text
<textarea name="heroLead" defaultValue={c.heroLead} required />
</label>
<div className="stack" style={{ gap: "1rem" }}>
<label style={{ flex: "1 1 220px" }}>
Primär-Button Text
<input name="primaryLabel" defaultValue={c.primaryCta.label} required />
</label>
<label style={{ flex: "1 1 220px" }}>
Primär-Button Link
<input name="primaryHref" defaultValue={c.primaryCta.href} required />
</label>
</div>
<div className="stack" style={{ gap: "1rem" }}>
<label style={{ flex: "1 1 220px" }}>
Sekundär-Button Text (optional)
<input name="secondaryLabel" defaultValue={c.secondaryCta?.label ?? ""} />
</label>
<label style={{ flex: "1 1 220px" }}>
Sekundär-Button Link (optional)
<input name="secondaryHref" defaultValue={c.secondaryCta?.href ?? ""} />
</label>
</div>
<label>
Abschnittstitel Vorteile
<input name="benefitSectionTitle" defaultValue={c.benefitSectionTitle} required />
</label>
{benefits.map((b, idx) => (
<div key={idx} className="panel" style={{ padding: "1rem" }}>
<div className="muted" style={{ marginBottom: "0.5rem", fontWeight: 700 }}>
Vorteil {idx + 1}
</div>
<label>
Titel
<input name={`benefit${idx + 1}Title`} defaultValue={b.title} />
</label>
<label>
Text
<textarea name={`benefit${idx + 1}Body`} defaultValue={b.body} />
</label>
</div>
))}
<button type="submit" className="btn btn-primary">
Speichern
</button>
</form>
</div>
);
}