Initial commit: FL-Akademie LMS mit Docker, Admin, Portal und Dokumentation.
Made-with: Cursor
This commit is contained in:
18
app/portal/account/page.tsx
Normal file
18
app/portal/account/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PasswordChangeForm } from "@/components/password-change-form";
|
||||
import { requireSession } from "@/lib/session-helpers";
|
||||
|
||||
export default async function PortalAccountPage() {
|
||||
const session = await requireSession();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="page-title">Konto</h1>
|
||||
<p className="muted subtitle">
|
||||
Angemeldet als <strong>{session.user.email}</strong>
|
||||
</p>
|
||||
|
||||
<h2 style={{ marginTop: "2rem" }}>Passwort ändern</h2>
|
||||
<PasswordChangeForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
app/portal/certificates/page.tsx
Normal file
54
app/portal/certificates/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireSession } from "@/lib/session-helpers";
|
||||
|
||||
export default async function PortalCertificatesPage() {
|
||||
const session = await requireSession();
|
||||
|
||||
const rows = await prisma.certificate.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { issuedAt: "desc" },
|
||||
include: { course: { select: { title: true, slug: true } } },
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="page-title">Zertifikate</h1>
|
||||
<p className="muted subtitle">Teilnahmebestätigungen nach vollständigem Kursabschluss.</p>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="panel">
|
||||
<p className="muted">Noch keine Zertifikate – schließe zuerst alle Lektionen eines Kurses ab.</p>
|
||||
<Link href="/portal" className="btn btn-primary">
|
||||
Zu deinen Kursen
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-wrap">
|
||||
<table className="simple">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kurs</th>
|
||||
<th>Ausgestellt</th>
|
||||
<th>Code</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>{r.course.title}</td>
|
||||
<td className="muted">{r.issuedAt.toLocaleDateString("de-DE")}</td>
|
||||
<td className="muted">{r.code}</td>
|
||||
<td>
|
||||
<Link href={`/zertifikat/${r.code}`}>Ansehen / Drucken</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
app/portal/layout.tsx
Normal file
25
app/portal/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Link from "next/link";
|
||||
import { requireSession } from "@/lib/session-helpers";
|
||||
|
||||
export default async function PortalLayout({ children }: { children: React.ReactNode }) {
|
||||
const session = await requireSession();
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<div className="container layout-split">
|
||||
<aside className="side-nav" aria-label="Mitgliederbereich">
|
||||
<strong style={{ display: "block", padding: "0.35rem 0.65rem", marginBottom: "0.35rem" }}>
|
||||
Mitgliederbereich
|
||||
</strong>
|
||||
<Link href="/portal">Übersicht</Link>
|
||||
<Link href="/portal/account">Konto & Passwort</Link>
|
||||
<Link href="/portal/certificates">Zertifikate</Link>
|
||||
<Link href="/kurse">Kurskatalog</Link>
|
||||
{session.user.role === "ADMIN" ? <Link href="/admin">Administration</Link> : null}
|
||||
<Link href="/">Startseite</Link>
|
||||
</aside>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
93
app/portal/page.tsx
Normal file
93
app/portal/page.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireSession } from "@/lib/session-helpers";
|
||||
import { firstLessonPath } from "@/lib/course-queries";
|
||||
import { getUserCourseProgress } from "@/lib/course-progress";
|
||||
import { RestartCourseButton } from "@/components/restart-course-button";
|
||||
|
||||
export default async function PortalHomePage() {
|
||||
const session = await requireSession();
|
||||
|
||||
const enrollments = await prisma.enrollment.findMany({
|
||||
where: { userId: session.user.id },
|
||||
include: {
|
||||
course: {
|
||||
include: {
|
||||
modules: {
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: {
|
||||
lessons: {
|
||||
where: { published: true },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
select: { slug: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
const certs = await prisma.certificate.findMany({
|
||||
where: { userId: session.user.id },
|
||||
select: { courseId: true, code: true },
|
||||
});
|
||||
const certByCourse = new Map(certs.map((c) => [c.courseId, c.code]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="page-title">Hallo{session.user.name ? `, ${session.user.name}` : ""}!</h1>
|
||||
<p className="muted subtitle">Deine Kurse, Fortschritt und nächste Schritte.</p>
|
||||
|
||||
{enrollments.length === 0 ? (
|
||||
<div className="panel">
|
||||
<p>Du bist noch in keinem Kurs eingeschrieben.</p>
|
||||
<Link href="/kurse" className="btn btn-primary">
|
||||
Kurse entdecken
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="course-grid" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))" }}>
|
||||
{await Promise.all(
|
||||
enrollments.map(async (e) => {
|
||||
const p = await getUserCourseProgress(session.user.id, e.course.id);
|
||||
const start = firstLessonPath(e.course);
|
||||
const cert = certByCourse.get(e.course.id);
|
||||
|
||||
return (
|
||||
<div key={e.id} className="panel">
|
||||
<h2 style={{ marginTop: 0, fontSize: "1.15rem" }}>{e.course.title}</h2>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
Fortschritt: {p.completed}/{p.total} Lektionen ({p.percent}%)
|
||||
</p>
|
||||
<div className="progress" aria-hidden="true">
|
||||
<span style={{ width: `${p.percent}%` }} />
|
||||
</div>
|
||||
<div className="stack" style={{ marginTop: "1rem" }}>
|
||||
{start ? (
|
||||
<Link href={start} className="btn btn-primary">
|
||||
{p.percent >= 100 ? "Kurs wiederholen" : "Weiterlernen"}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="muted">Keine Lektionen</span>
|
||||
)}
|
||||
<Link href={`/kurse/${e.course.slug}`} className="btn btn-ghost">
|
||||
Kurrikulum
|
||||
</Link>
|
||||
{cert ? (
|
||||
<Link href={`/zertifikat/${cert}`} className="btn btn-ghost">
|
||||
Zertifikat
|
||||
</Link>
|
||||
) : null}
|
||||
<RestartCourseButton courseId={e.course.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user