fix: remove middleware and route groups to fix Vercel deployment
- Remove middleware.ts entirely (caused __dirname ReferenceError in Edge) - Auth protection handled by dashboard layout.tsx (server-side redirect) - Move pages out of (marketing) and (auth) route groups to fix 404 on / - Keep (protected) route group for dashboard/formations/profil shared layout https://claude.ai/code/session_01H2aRGDaKgarPvhay2HxN6Y
This commit is contained in:
133
app/(protected)/dashboard/page.tsx
Normal file
133
app/(protected)/dashboard/page.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import Card from "@/components/ui/Card";
|
||||
import ProgressBar from "@/components/dashboard/ProgressBar";
|
||||
import ModuleCard from "@/components/dashboard/ModuleCard";
|
||||
import type { Module, UserProgress, Profile } from "@/types/database.types";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
// Récupérer le profil
|
||||
const { data: profile } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user!.id)
|
||||
.single() as { data: Profile | null };
|
||||
|
||||
// Récupérer les modules publiés
|
||||
const { data: modules } = await supabase
|
||||
.from("modules")
|
||||
.select("*")
|
||||
.eq("is_published", true)
|
||||
.order("week_number", { ascending: true })
|
||||
.order("order_index", { ascending: true }) as { data: Module[] | null };
|
||||
|
||||
// Récupérer la progression
|
||||
const { data: progress } = await supabase
|
||||
.from("user_progress")
|
||||
.select("*")
|
||||
.eq("user_id", user!.id) as { data: UserProgress[] | null };
|
||||
|
||||
const totalModules = modules?.length || 0;
|
||||
const completedModules =
|
||||
progress?.filter((p) => p.completed).length || 0;
|
||||
const progressPercent =
|
||||
totalModules > 0 ? (completedModules / totalModules) * 100 : 0;
|
||||
|
||||
// Prochain module non complété
|
||||
const completedIds = new Set(
|
||||
progress?.filter((p) => p.completed).map((p) => p.module_id)
|
||||
);
|
||||
const nextModules =
|
||||
modules?.filter((m) => !completedIds.has(m.id)).slice(0, 3) || [];
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl">
|
||||
{/* Header */}
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">
|
||||
Bonjour {profile?.full_name?.split(" ")[0] || "!"} 👋
|
||||
</h1>
|
||||
<p className="text-white/60">
|
||||
Voici un apercu de ta progression dans le programme.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-10">
|
||||
<Card>
|
||||
<p className="text-white/40 text-sm mb-1">Progression globale</p>
|
||||
<p className="text-2xl font-bold text-white mb-3">
|
||||
{Math.round(progressPercent)}%
|
||||
</p>
|
||||
<ProgressBar value={progressPercent} showPercentage={false} />
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-white/40 text-sm mb-1">Modules completes</p>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{completedModules}
|
||||
<span className="text-white/30 text-lg font-normal">
|
||||
/{totalModules}
|
||||
</span>
|
||||
</p>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-white/40 text-sm mb-1">Statut abonnement</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-success rounded-full" />
|
||||
<p className="text-success font-semibold">Actif</p>
|
||||
</div>
|
||||
{profile?.subscription_end_date && (
|
||||
<p className="text-white/30 text-xs mt-1">
|
||||
Jusqu'au{" "}
|
||||
{new Date(profile.subscription_end_date).toLocaleDateString(
|
||||
"fr-FR"
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Prochains modules */}
|
||||
{nextModules.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-4">
|
||||
Continue ta formation
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{nextModules.map((module) => {
|
||||
const moduleProgress = progress?.find(
|
||||
(p) => p.module_id === module.id
|
||||
);
|
||||
return (
|
||||
<ModuleCard
|
||||
key={module.id}
|
||||
module={module}
|
||||
progress={moduleProgress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message si aucun module */}
|
||||
{totalModules === 0 && (
|
||||
<Card className="text-center py-12">
|
||||
<div className="text-4xl mb-4">🚀</div>
|
||||
<h3 className="text-white font-semibold text-lg mb-2">
|
||||
Le programme arrive bientot !
|
||||
</h3>
|
||||
<p className="text-white/40 text-sm max-w-md mx-auto">
|
||||
Les modules de formation sont en cours de preparation. Tu seras
|
||||
notifie des qu'ils seront disponibles.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
app/(protected)/formations/[moduleId]/MarkCompleteButton.tsx
Normal file
83
app/(protected)/formations/[moduleId]/MarkCompleteButton.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
interface MarkCompleteButtonProps {
|
||||
moduleId: string;
|
||||
userId: string;
|
||||
isCompleted: boolean;
|
||||
}
|
||||
|
||||
export default function MarkCompleteButton({
|
||||
moduleId,
|
||||
userId,
|
||||
isCompleted: initialCompleted,
|
||||
}: MarkCompleteButtonProps) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [completed, setCompleted] = useState(initialCompleted);
|
||||
|
||||
const handleToggle = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const supabase = createClient();
|
||||
|
||||
if (completed) {
|
||||
// Marquer comme non complété
|
||||
await (supabase
|
||||
.from("user_progress")
|
||||
.update({ completed: false, completed_at: null } as never)
|
||||
.eq("user_id", userId)
|
||||
.eq("module_id", moduleId));
|
||||
} else {
|
||||
// Marquer comme complété (upsert)
|
||||
await (supabase.from("user_progress").upsert({
|
||||
user_id: userId,
|
||||
module_id: moduleId,
|
||||
completed: true,
|
||||
completed_at: new Date().toISOString(),
|
||||
} as never));
|
||||
}
|
||||
|
||||
setCompleted(!completed);
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
console.error("Erreur mise a jour progression:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleToggle}
|
||||
loading={loading}
|
||||
variant={completed ? "secondary" : "primary"}
|
||||
>
|
||||
{completed ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-success"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Complete - Annuler
|
||||
</span>
|
||||
) : (
|
||||
"Marquer comme complete"
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
181
app/(protected)/formations/[moduleId]/page.tsx
Normal file
181
app/(protected)/formations/[moduleId]/page.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
108
app/(protected)/formations/page.tsx
Normal file
108
app/(protected)/formations/page.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import ModuleCard from "@/components/dashboard/ModuleCard";
|
||||
import ProgressBar from "@/components/dashboard/ProgressBar";
|
||||
import type { Module, UserProgress } from "@/types/database.types";
|
||||
|
||||
export default async function FormationsPage() {
|
||||
const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
// Récupérer les modules publiés
|
||||
const { data: modules } = await supabase
|
||||
.from("modules")
|
||||
.select("*")
|
||||
.eq("is_published", true)
|
||||
.order("week_number", { ascending: true })
|
||||
.order("order_index", { ascending: true }) as { data: Module[] | null };
|
||||
|
||||
// Récupérer la progression
|
||||
const { data: progress } = await supabase
|
||||
.from("user_progress")
|
||||
.select("*")
|
||||
.eq("user_id", user!.id) as { data: UserProgress[] | null };
|
||||
|
||||
// Grouper les modules par semaine
|
||||
const modulesByWeek = (modules || []).reduce(
|
||||
(acc, module) => {
|
||||
const week = module.week_number;
|
||||
if (!acc[week]) acc[week] = [];
|
||||
acc[week].push(module);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, Module[]>
|
||||
);
|
||||
|
||||
const totalModules = modules?.length || 0;
|
||||
const completedModules =
|
||||
progress?.filter((p) => p.completed).length || 0;
|
||||
const progressPercent =
|
||||
totalModules > 0 ? (completedModules / totalModules) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl">
|
||||
{/* Header */}
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Formations</h1>
|
||||
<p className="text-white/60 mb-6">
|
||||
Progression dans le programme HookLab - 8 semaines.
|
||||
</p>
|
||||
<ProgressBar
|
||||
value={progressPercent}
|
||||
label={`${completedModules} modules completes sur ${totalModules}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modules par semaine */}
|
||||
{Object.entries(modulesByWeek).map(([week, weekModules]) => {
|
||||
const weekCompleted =
|
||||
weekModules?.filter((m) =>
|
||||
progress?.find((p) => p.module_id === m.id && p.completed)
|
||||
).length || 0;
|
||||
const weekTotal = weekModules?.length || 0;
|
||||
|
||||
return (
|
||||
<div key={week} className="mb-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
Semaine {week}
|
||||
</h2>
|
||||
<span className="text-white/30 text-sm">
|
||||
{weekCompleted}/{weekTotal} completes
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{weekModules?.map((module) => {
|
||||
const moduleProgress = progress?.find(
|
||||
(p) => p.module_id === module.id
|
||||
);
|
||||
return (
|
||||
<ModuleCard
|
||||
key={module.id}
|
||||
module={module}
|
||||
progress={moduleProgress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Message si aucun module */}
|
||||
{totalModules === 0 && (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-5xl mb-4">📚</div>
|
||||
<h3 className="text-white font-semibold text-lg mb-2">
|
||||
Aucun module disponible
|
||||
</h3>
|
||||
<p className="text-white/40 text-sm">
|
||||
Les modules de formation seront bientot disponibles.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
app/(protected)/layout.tsx
Normal file
44
app/(protected)/layout.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import Sidebar from "@/components/dashboard/Sidebar";
|
||||
import type { Profile } from "@/types/database.types";
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Vérifier l'authentification
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
// Récupérer le profil
|
||||
const { data: profile } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single() as { data: Profile | null };
|
||||
|
||||
if (!profile) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
// Vérifier l'abonnement actif
|
||||
if (profile.subscription_status !== "active") {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar user={profile} />
|
||||
<main className="flex-1 p-6 md:p-10 overflow-y-auto">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
240
app/(protected)/profil/page.tsx
Normal file
240
app/(protected)/profil/page.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Input from "@/components/ui/Input";
|
||||
import Card from "@/components/ui/Card";
|
||||
import type { Profile } from "@/types/database.types";
|
||||
|
||||
export default function ProfilPage() {
|
||||
const router = useRouter();
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [fullName, setFullName] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState<{
|
||||
type: "success" | "error";
|
||||
text: string;
|
||||
} | null>(null);
|
||||
|
||||
// Changement de mot de passe
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState("");
|
||||
const [passwordSaving, setPasswordSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
setLoading(true);
|
||||
const supabase = createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single() as { data: Profile | null };
|
||||
|
||||
if (data) {
|
||||
setProfile(data);
|
||||
setFullName(data.full_name || "");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
}, []);
|
||||
|
||||
const handleSaveProfile = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update({ full_name: fullName } as never)
|
||||
.eq("id", profile!.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setMessage({ type: "success", text: "Profil mis a jour !" });
|
||||
router.refresh();
|
||||
} catch {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Erreur lors de la mise a jour du profil.",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setPasswordSaving(true);
|
||||
setMessage(null);
|
||||
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Les mots de passe ne correspondent pas.",
|
||||
});
|
||||
setPasswordSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Le mot de passe doit contenir au moins 8 caracteres.",
|
||||
});
|
||||
setPasswordSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setMessage({ type: "success", text: "Mot de passe mis a jour !" });
|
||||
setNewPassword("");
|
||||
setConfirmNewPassword("");
|
||||
} catch {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Erreur lors du changement de mot de passe.",
|
||||
});
|
||||
} finally {
|
||||
setPasswordSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Mon profil</h1>
|
||||
<p className="text-white/60 mb-10">
|
||||
Gere tes informations personnelles et ton abonnement.
|
||||
</p>
|
||||
|
||||
{/* Message */}
|
||||
{message && (
|
||||
<div
|
||||
className={`mb-6 p-3 rounded-xl border ${
|
||||
message.type === "success"
|
||||
? "bg-success/10 border-success/20 text-success"
|
||||
: "bg-error/10 border-error/20 text-error"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{message.text}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informations profil */}
|
||||
<Card className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6">
|
||||
Informations personnelles
|
||||
</h2>
|
||||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
||||
<Input
|
||||
id="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
value={profile?.email || ""}
|
||||
disabled
|
||||
className="opacity-50"
|
||||
/>
|
||||
<Input
|
||||
id="fullName"
|
||||
label="Nom complet"
|
||||
placeholder="Jean Dupont"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" loading={saving}>
|
||||
Sauvegarder
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* Changement de mot de passe */}
|
||||
<Card className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6">
|
||||
Changer le mot de passe
|
||||
</h2>
|
||||
<form onSubmit={handleChangePassword} className="space-y-5">
|
||||
<Input
|
||||
id="newPassword"
|
||||
label="Nouveau mot de passe"
|
||||
type="password"
|
||||
placeholder="Minimum 8 caracteres"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
id="confirmNewPassword"
|
||||
label="Confirmer le mot de passe"
|
||||
type="password"
|
||||
placeholder="Confirme ton nouveau mot de passe"
|
||||
value={confirmNewPassword}
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" loading={passwordSaving} variant="secondary">
|
||||
Changer le mot de passe
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* Abonnement */}
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Abonnement</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/60 text-sm">Statut</span>
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-success/10 rounded-lg text-success text-sm font-medium">
|
||||
<span className="w-1.5 h-1.5 bg-success rounded-full" />
|
||||
{profile?.subscription_status === "active"
|
||||
? "Actif"
|
||||
: "Inactif"}
|
||||
</span>
|
||||
</div>
|
||||
{profile?.subscription_end_date && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/60 text-sm">Valide jusqu'au</span>
|
||||
<span className="text-white text-sm">
|
||||
{new Date(profile.subscription_end_date).toLocaleDateString(
|
||||
"fr-FR",
|
||||
{ day: "numeric", month: "long", year: "numeric" }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/60 text-sm">Plan</span>
|
||||
<span className="text-white text-sm">
|
||||
HookLab - Programme 8 semaines
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user