Initial commit: FL-Akademie LMS mit Docker, Admin, Portal und Dokumentation.
Made-with: Cursor
This commit is contained in:
51
app/admin/landing/actions.ts
Normal file
51
app/admin/landing/actions.ts
Normal 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");
|
||||
}
|
||||
75
app/admin/landing/page.tsx
Normal file
75
app/admin/landing/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user