Files
obc-terrassement/app/(dashboard)/formations/[moduleId]/page.tsx
Claude 41e686c560 feat: complete HookLab MVP - TikTok Shop coaching platform
Full-stack Next.js 15 application with:
- Landing page with marketing components (Hero, Testimonials, Pricing, FAQ)
- Multi-step candidature form with API route
- Stripe Checkout integration (subscription + webhooks)
- Supabase Auth (login/register) with middleware protection
- Dashboard with progress tracking and module system
- Formations pages with completion tracking
- Profile management with password change
- Database schema with RLS policies
- Resend email integration for transactional emails

Stack: Next.js 15, TypeScript, Tailwind CSS v4, Supabase, Stripe, Resend

https://claude.ai/code/session_01H2aRGDaKgarPvhay2HxN6Y
2026-02-08 12:39:18 +00:00

182 lines
5.8 KiB
TypeScript

import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import Card from "@/components/ui/Card";
import MarkCompleteButton from "./MarkCompleteButton";
import type { Module, UserProgress } from "@/types/database.types";
interface ModulePageProps {
params: Promise<{ moduleId: string }>;
}
export default async function ModulePage({ params }: ModulePageProps) {
const { moduleId } = await params;
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
// Récupérer le module
const { data: module } = await supabase
.from("modules")
.select("*")
.eq("id", moduleId)
.eq("is_published", true)
.single() as { data: Module | null };
if (!module) {
redirect("/formations");
}
// Récupérer la progression pour ce module
const { data: progress } = await supabase
.from("user_progress")
.select("*")
.eq("user_id", user!.id)
.eq("module_id", moduleId)
.single() as { data: UserProgress | null };
return (
<div className="max-w-4xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm mb-8">
<a
href="/formations"
className="text-white/40 hover:text-white transition-colors"
>
Formations
</a>
<span className="text-white/20">/</span>
<span className="text-white/40">Semaine {module.week_number}</span>
<span className="text-white/20">/</span>
<span className="text-white">{module.title}</span>
</nav>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-3">
<span className="inline-flex items-center px-2.5 py-1 bg-primary/10 rounded-lg text-primary text-xs font-medium">
Semaine {module.week_number}
</span>
{module.content_type && (
<span className="inline-flex items-center px-2.5 py-1 bg-dark-lighter rounded-lg text-white/40 text-xs font-medium uppercase">
{module.content_type}
</span>
)}
{module.duration_minutes && (
<span className="text-white/30 text-xs">
{module.duration_minutes} min
</span>
)}
{progress?.completed && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-success/10 rounded-lg text-success text-xs font-medium">
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
Complete
</span>
)}
</div>
<h1 className="text-3xl font-bold text-white mb-3">{module.title}</h1>
{module.description && (
<p className="text-white/60 text-lg">{module.description}</p>
)}
</div>
{/* Contenu du module */}
<Card className="mb-8">
{/* Video */}
{module.content_type === "video" && module.content_url && (
<div className="aspect-video bg-dark-lighter rounded-2xl overflow-hidden mb-6">
<iframe
src={module.content_url}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title={module.title}
/>
</div>
)}
{/* PDF */}
{module.content_type === "pdf" && module.content_url && (
<div className="mb-6">
<a
href={module.content_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-3 bg-primary/10 text-primary rounded-xl hover:bg-primary/20 transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
Telecharger le PDF
</a>
</div>
)}
{/* Placeholder si pas de contenu */}
{!module.content_url && (
<div className="aspect-video bg-dark-lighter rounded-2xl flex items-center justify-center mb-6">
<div className="text-center">
<div className="text-4xl mb-3">🎬</div>
<p className="text-white/40 text-sm">
Le contenu sera bientot disponible
</p>
</div>
</div>
)}
</Card>
{/* Actions */}
<div className="flex items-center justify-between">
<a
href="/formations"
className="text-white/40 hover:text-white text-sm transition-colors flex items-center gap-1"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Retour aux formations
</a>
<MarkCompleteButton
moduleId={moduleId}
userId={user!.id}
isCompleted={progress?.completed || false}
/>
</div>
</div>
);
}