358 lines
12 KiB
TypeScript
358 lines
12 KiB
TypeScript
import { getTranslations } from "next-intl/server";
|
|
import { notFound } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { db } from "@/lib/db";
|
|
import { auth } from "@/auth";
|
|
import { ProgressBar } from "@/components/ProgressBar";
|
|
import { EnrollButton } from "./EnrollButton";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const levelLabels: Record<string, Record<string, string>> = {
|
|
fr: { BEGINNER: "Débutant", INTERMEDIATE: "Intermédiaire", ADVANCED: "Avancé" },
|
|
en: { BEGINNER: "Beginner", INTERMEDIATE: "Intermediate", ADVANCED: "Advanced" },
|
|
es: { BEGINNER: "Principiante", INTERMEDIATE: "Intermedio", ADVANCED: "Avanzado" },
|
|
};
|
|
|
|
function getLocaleText(
|
|
obj: Record<string, string>,
|
|
locale: string,
|
|
keys: { fr: string; en: string; es: string }
|
|
) {
|
|
if (locale === "en") return (obj as any)[keys.en] || (obj as any)[keys.fr];
|
|
if (locale === "es") return (obj as any)[keys.es] || (obj as any)[keys.fr];
|
|
return (obj as any)[keys.fr];
|
|
}
|
|
|
|
export default async function CourseDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ locale: string; slug: string }>;
|
|
}) {
|
|
const { locale, slug } = await params;
|
|
const t = await getTranslations({ locale, namespace: "course" });
|
|
|
|
const course = await db.course.findUnique({
|
|
where: { slug },
|
|
include: {
|
|
modules: {
|
|
orderBy: { order: "asc" },
|
|
include: {
|
|
lessons: { orderBy: { order: "asc" } },
|
|
quiz: { select: { id: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!course || !course.published) {
|
|
notFound();
|
|
}
|
|
|
|
const session = await auth();
|
|
let enrollment = null;
|
|
let progress = 0;
|
|
let firstLessonId: string | null = null;
|
|
|
|
if (session?.user) {
|
|
const userId = (session.user as any).id;
|
|
enrollment = await db.enrollment.findUnique({
|
|
where: { userId_courseId: { userId, courseId: course.id } },
|
|
});
|
|
|
|
// Calculate progress
|
|
const allLessons = course.modules.flatMap((m) => m.lessons);
|
|
firstLessonId = allLessons[0]?.id ?? null;
|
|
|
|
if (allLessons.length > 0) {
|
|
const completedCount = await db.lessonProgress.count({
|
|
where: {
|
|
userId,
|
|
lessonId: { in: allLessons.map((l) => l.id) },
|
|
},
|
|
});
|
|
progress = Math.round((completedCount / allLessons.length) * 100);
|
|
}
|
|
}
|
|
|
|
const title = getLocaleText(course as any, locale, { fr: "titleFr", en: "titleEn", es: "titleEs" });
|
|
const desc = getLocaleText(course as any, locale, { fr: "descFr", en: "descEn", es: "descEs" });
|
|
const lvlLabel = (levelLabels[locale] ?? levelLabels.fr)[course.level];
|
|
|
|
const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0);
|
|
|
|
return (
|
|
<div style={{ maxWidth: 1000, margin: "0 auto", padding: "40px 24px" }}>
|
|
{/* Breadcrumb */}
|
|
<div style={{ marginBottom: 24, fontSize: 13, color: "#94a3b8" }}>
|
|
<Link href={`/${locale}/courses`} style={{ color: "#60a5fa" }}>
|
|
Formations
|
|
</Link>{" "}
|
|
/ {title}
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 320px", gap: 32, alignItems: "start" }}>
|
|
{/* Main content */}
|
|
<div>
|
|
{/* Course header */}
|
|
<div style={{ marginBottom: 32 }}>
|
|
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
|
|
<span
|
|
style={{
|
|
background: "rgba(29,78,216,0.2)",
|
|
color: "#60a5fa",
|
|
borderRadius: 999,
|
|
padding: "3px 12px",
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
{course.category}
|
|
</span>
|
|
<span
|
|
style={{
|
|
background: "rgba(245,158,11,0.15)",
|
|
color: "#fbbf24",
|
|
borderRadius: 999,
|
|
padding: "3px 12px",
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
{lvlLabel}
|
|
</span>
|
|
</div>
|
|
<h1 style={{ fontSize: 28, fontWeight: 800, color: "#f1f5f9", marginBottom: 16, lineHeight: 1.3 }}>
|
|
{title}
|
|
</h1>
|
|
<p style={{ fontSize: 16, color: "#94a3b8", lineHeight: 1.7 }}>
|
|
{desc}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Thumbnail */}
|
|
{course.thumbnailUrl && (
|
|
<div
|
|
style={{
|
|
borderRadius: 12,
|
|
overflow: "hidden",
|
|
marginBottom: 32,
|
|
aspectRatio: "16/9",
|
|
background: "#1a1f2e",
|
|
}}
|
|
>
|
|
<img
|
|
src={course.thumbnailUrl}
|
|
alt={title}
|
|
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Module list */}
|
|
<div>
|
|
<h2 style={{ fontSize: 20, fontWeight: 700, color: "#f1f5f9", marginBottom: 16 }}>
|
|
{t("modules")} ({course.modules.length})
|
|
</h2>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
{course.modules.map((module, mIndex) => {
|
|
const moduleTitle = getLocaleText(module as any, locale, {
|
|
fr: "titleFr",
|
|
en: "titleEn",
|
|
es: "titleEs",
|
|
});
|
|
return (
|
|
<div
|
|
key={module.id}
|
|
style={{
|
|
background: "#1a1f2e",
|
|
border: "1px solid rgba(255,255,255,0.08)",
|
|
borderRadius: 10,
|
|
padding: "16px 20px",
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
<div
|
|
style={{
|
|
width: 28,
|
|
height: 28,
|
|
background: "rgba(29,78,216,0.2)",
|
|
color: "#60a5fa",
|
|
borderRadius: 6,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{mIndex + 1}
|
|
</div>
|
|
<span style={{ fontSize: 15, fontWeight: 600, color: "#f1f5f9" }}>
|
|
{moduleTitle}
|
|
</span>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
<span style={{ fontSize: 12, color: "#94a3b8" }}>
|
|
{module.lessons.length} {t("lessons")}
|
|
</span>
|
|
{module.quiz && (
|
|
<span
|
|
style={{
|
|
fontSize: 11,
|
|
fontWeight: 600,
|
|
background: "rgba(245,158,11,0.15)",
|
|
color: "#fbbf24",
|
|
borderRadius: 4,
|
|
padding: "2px 8px",
|
|
}}
|
|
>
|
|
{t("quiz")}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{module.lessons.length > 0 && (
|
|
<div style={{ marginTop: 10, paddingLeft: 40 }}>
|
|
{module.lessons.map((lesson) => {
|
|
const lessonTitle = getLocaleText(lesson as any, locale, {
|
|
fr: "titleFr",
|
|
en: "titleEn",
|
|
es: "titleEs",
|
|
});
|
|
return (
|
|
<div
|
|
key={lesson.id}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "6px 0",
|
|
borderTop: "1px solid rgba(255,255,255,0.04)",
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 12, color: "#475569" }}>
|
|
{lesson.type === "VIDEO" ? "▶" : "📄"}
|
|
</span>
|
|
<span style={{ fontSize: 13, color: "#94a3b8" }}>{lessonTitle}</span>
|
|
{lesson.duration && (
|
|
<span style={{ fontSize: 12, color: "#475569", marginLeft: "auto" }}>
|
|
{Math.floor(lesson.duration / 60)}min
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div style={{ position: "sticky", top: 88 }}>
|
|
<div className="card">
|
|
{/* Stats */}
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr 1fr",
|
|
gap: 16,
|
|
marginBottom: 20,
|
|
paddingBottom: 20,
|
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
|
}}
|
|
>
|
|
<div>
|
|
<div style={{ fontSize: 22, fontWeight: 700, color: "#f1f5f9" }}>
|
|
{course.modules.length}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: "#94a3b8" }}>{t("modules")}</div>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 22, fontWeight: 700, color: "#f1f5f9" }}>
|
|
{totalLessons}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: "#94a3b8" }}>{t("lessons")}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress if enrolled */}
|
|
{enrollment && (
|
|
<div style={{ marginBottom: 20 }}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
fontSize: 13,
|
|
color: "#94a3b8",
|
|
marginBottom: 8,
|
|
}}
|
|
>
|
|
<span>Progression</span>
|
|
<span style={{ fontWeight: 700, color: "#f1f5f9" }}>{progress}%</span>
|
|
</div>
|
|
<ProgressBar value={progress} height={8} />
|
|
{enrollment.completedAt && (
|
|
<p
|
|
style={{
|
|
fontSize: 12,
|
|
color: "#4ade80",
|
|
marginTop: 8,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 4,
|
|
}}
|
|
>
|
|
✓ Formation complétée
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* CTA */}
|
|
{enrollment ? (
|
|
<Link
|
|
href={`/${locale}/courses/${slug}/learn`}
|
|
className="btn btn-primary"
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
padding: "12px",
|
|
fontSize: 15,
|
|
}}
|
|
>
|
|
{progress > 0 ? t("continue") : t("start")} →
|
|
</Link>
|
|
) : session ? (
|
|
<EnrollButton
|
|
courseId={course.id}
|
|
locale={locale}
|
|
slug={slug}
|
|
label={t("enroll")}
|
|
/>
|
|
) : (
|
|
<Link
|
|
href={`/${locale}/auth/login`}
|
|
className="btn btn-primary"
|
|
style={{ display: "flex", justifyContent: "center", padding: "12px", fontSize: 15 }}
|
|
>
|
|
{t("enroll")}
|
|
</Link>
|
|
)}
|
|
|
|
<p style={{ fontSize: 12, color: "#475569", textAlign: "center", marginTop: 12 }}>
|
|
Accès illimité · Certificat inclus
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|