From d2696cd09b9b7a2233556e8c59466eb10e426b72 Mon Sep 17 00:00:00 2001 From: LucasRossignon Date: Fri, 24 Oct 2025 22:40:31 +0200 Subject: [PATCH 01/87] FEAT/ResetPassword --- app/(auth)/reset-password/page.tsx | 31 ++ .../auth/reset-password-with-token-form.tsx | 314 ++++++++++++++++++ lib/actions/auth.ts | 96 +++++- package-lock.json | 160 ++++++--- 4 files changed, 542 insertions(+), 59 deletions(-) create mode 100644 app/(auth)/reset-password/page.tsx create mode 100644 components/auth/reset-password-with-token-form.tsx diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..1ed641b --- /dev/null +++ b/app/(auth)/reset-password/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Suspense } from "react"; +import ResetPasswordForm from "@/components/auth/reset-password-with-token-form"; + +function ResetPasswordContent() { + return ( +
+
+
+ +
+
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + +
+
+

Chargement...

+
+
+ }> + +
+ ); +} \ No newline at end of file diff --git a/components/auth/reset-password-with-token-form.tsx b/components/auth/reset-password-with-token-form.tsx new file mode 100644 index 0000000..1b429ac --- /dev/null +++ b/components/auth/reset-password-with-token-form.tsx @@ -0,0 +1,314 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Eye, EyeOff, KeyRound, CheckCircle2, XCircle, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { FormErrorAlert } from "@/components/ui/form-alert"; +import { verifyResetTokenAction, resetPasswordWithTokenAction } from "@/lib/actions/auth"; +import { getPasswordRequirements } from "@/lib/schemas/auth"; +import { showToast } from "@/lib/toast"; +import Link from "next/link"; + +// Schema for password reset with token +const resetPasswordSchema = z.object({ + newPassword: z + .string() + .min(6, "Le mot de passe doit contenir au moins 6 caractères") + .regex( + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, + "Le mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre" + ), + confirmPassword: z.string(), +}).refine(data => data.newPassword === data.confirmPassword, { + message: "Les mots de passe ne correspondent pas", + path: ["confirmPassword"], +}); + +type ResetPasswordFormValues = z.infer; + +export default function ResetPasswordWithTokenForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [isLoading, setIsLoading] = useState(false); + const [isVerifying, setIsVerifying] = useState(true); + const [isTokenValid, setIsTokenValid] = useState(false); + const [maskedEmail, setMaskedEmail] = useState(""); + const [error, setError] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + watch, + } = useForm({ + resolver: zodResolver(resetPasswordSchema), + }); + + const newPassword = watch("newPassword", ""); + const passwordRequirements = getPasswordRequirements(newPassword); + + // Verify token on mount + useEffect(() => { + const verifyToken = async () => { + if (!token) { + setError("Aucun token de réinitialisation fourni"); + setIsVerifying(false); + return; + } + + try { + const response = await verifyResetTokenAction(token); + + if (response.success) { + setIsTokenValid(true); + setMaskedEmail(response.maskedEmail || ""); + } else { + setError(response.error || "Token invalide ou expiré"); + } + } catch (err) { + setError("Une erreur est survenue lors de la vérification du token"); + } finally { + setIsVerifying(false); + } + }; + + verifyToken(); + }, [token]); + + const onSubmit = async (data: ResetPasswordFormValues) => { + if (!token) { + setError("Token manquant"); + return; + } + + setIsLoading(true); + setError(""); + + try { + const response = await resetPasswordWithTokenAction(token, data.newPassword); + + if (response.success) { + setIsSuccess(true); + showToast.success("Mot de passe réinitialisé avec succès !"); + + // Redirect to login after 3 seconds + setTimeout(() => { + router.push("/auth"); + }, 3000); + } else { + setError(response.error || "Une erreur est survenue"); + } + } catch (err) { + setError("Une erreur est survenue lors de la réinitialisation"); + } finally { + setIsLoading(false); + } + }; + + // Loading state while verifying token + if (isVerifying) { + return ( + + + +

Vérification du lien de réinitialisation...

+
+
+ ); + } + + // Invalid token state + if (!isTokenValid) { + return ( + + + + Lien invalide ou expiré + + {error || "Le lien de réinitialisation est invalide ou a expiré."} + + + +

+ Les liens de réinitialisation expirent après 30 minutes pour des raisons de sécurité. +

+
+ + + + +
+ ); + } + + // Success state + if (isSuccess) { + return ( + + + + Mot de passe réinitialisé ! + + Votre mot de passe a été modifié avec succès. + + + +

+ Vous allez être redirigé vers la page de connexion... +

+
+ + + +
+ ); + } + + // Reset form + return ( + + + + Créer un nouveau mot de passe + + {maskedEmail && ( + + Réinitialisation pour : {maskedEmail} + + )} + + +
+ + {error && } + + {/* New Password */} +
+ +
+ + +
+ {errors.newPassword && ( +

{errors.newPassword.message}

+ )} +
+ + {/* Password Requirements */} + {newPassword && ( +
+

Exigences du mot de passe :

+
+ {passwordRequirements.map((req, index) => ( +
+ {req.met ? ( + + ) : ( + + )} + + {req.label} + +
+ ))} +
+
+ )} + + {/* Confirm Password */} +
+ +
+ + +
+ {errors.confirmPassword && ( +

{errors.confirmPassword.message}

+ )} +
+
+ + + + +
+
+ ); +} \ No newline at end of file diff --git a/lib/actions/auth.ts b/lib/actions/auth.ts index f444a58..fbf7d3c 100644 --- a/lib/actions/auth.ts +++ b/lib/actions/auth.ts @@ -100,30 +100,42 @@ export async function signupAction( } } -// Reset password action +// Reset password action (request password reset email) export async function resetPasswordAction( data: ResetPasswordFormValues ): Promise { - try { - // TODO: Implement actual password reset logic - console.log("Reset password attempt:", { email: data.email }); + const apiUrl = process.env.NEXT_PUBLIC_API_URL; - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 800)); + try { + const response = await axios.post(`${apiUrl}/auth/forgot-password`, { + email: data.email, + }); - // Mock response - replace with actual password reset logic return { success: true, data: { - message: - "Un email de réinitialisation a été envoyé à votre adresse", + message: response.data.message || + "Si un compte existe avec cet email, vous recevrez un lien de réinitialisation.", }, }; - } catch (error) { + } catch (error: unknown) { console.error("Reset password error:", error); + + // Check if it's a rate limit error + const axiosError = error as AxiosError<{ message: string }>; + if (axiosError.response?.status === 429) { + return { + success: false, + error: "Trop de tentatives. Veuillez réessayer dans une heure.", + }; + } + return { success: false, - error: "Une erreur est survenue lors de la réinitialisation", + error: translateApiError( + axiosError.response?.data?.message || + "Une erreur est survenue lors de la réinitialisation" + ), }; } } @@ -163,6 +175,68 @@ export async function changePasswordAction( } } +// Verify reset token action +export async function verifyResetTokenAction( + token: string +): Promise { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + try { + const response = await axios.get(`${apiUrl}/auth/verify-reset-token/${token}`); + + return { + success: response.data.isValid, + data: response.data, + maskedEmail: response.data.maskedEmail, + }; + } catch (error: unknown) { + console.error("Verify reset token error:", error); + const axiosError = error as AxiosError<{ message: string }>; + + return { + success: false, + error: translateApiError( + axiosError.response?.data?.message || + "Le lien de réinitialisation est invalide ou expiré" + ), + }; + } +} + +// Reset password with token action +export async function resetPasswordWithTokenAction( + token: string, + newPassword: string +): Promise { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + try { + const response = await axios.post(`${apiUrl}/auth/reset-password`, { + token, + newPassword, + }); + + return { + success: true, + data: { + message: response.data.message || + "Mot de passe réinitialisé avec succès. Vous pouvez maintenant vous connecter.", + }, + }; + } catch (error: unknown) { + console.error("Reset password with token error:", error); + const axiosError = error as AxiosError<{ message: string }>; + + return { + success: false, + error: translateApiError( + axiosError.response?.data?.message || + "Une erreur est survenue lors de la réinitialisation du mot de passe" + ), + }; + } +} + // Signout action export async function signoutAction(): Promise { try { diff --git a/package-lock.json b/package-lock.json index 808cae7..455bbf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "marked": "^15.0.12", "next": "14.2.16", "next-themes": "^0.4.3", - "pdfjs-dist": "^5.3.31", + "pdfjs-dist": "^5.4.296", "react": "^18", "react-day-picker": "8.10.1", "react-dom": "^18", @@ -51,6 +51,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@playwright/test": "^1.55.1", "@types/node": "^20.17.6", "@types/react": "^18", "@types/react-dom": "^18", @@ -721,9 +722,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.72.tgz", - "integrity": "sha512-ypTJ/DXzsJbTU3o7qXFlWmZGgEbh42JWQl7v5/i+DJz/HURELcSnq9ler9e1ukqma70JzmCQcIseiE/Xs6sczw==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", "license": "MIT", "optional": true, "workspaces": [ @@ -733,22 +734,22 @@ "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.72", - "@napi-rs/canvas-darwin-arm64": "0.1.72", - "@napi-rs/canvas-darwin-x64": "0.1.72", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.72", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.72", - "@napi-rs/canvas-linux-arm64-musl": "0.1.72", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.72", - "@napi-rs/canvas-linux-x64-gnu": "0.1.72", - "@napi-rs/canvas-linux-x64-musl": "0.1.72", - "@napi-rs/canvas-win32-x64-msvc": "0.1.72" + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.72.tgz", - "integrity": "sha512-OW99TDJEdfOhpJWQ7SXFsQi1BXd6UFuWM8AoQvJ0SQMHWY/iwuopmb1UqGV6Df9aM/SWxvCWBN/onjeCM8KVKQ==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", "cpu": [ "arm64" ], @@ -762,9 +763,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.72.tgz", - "integrity": "sha512-gB8Pn/4GdS+B6P4HYuNqPGx8iQJ16Go1D6e5hIxfUbA/efupVGZ7e3OMGWGCUgF0vgbEPEF31sPzhcad4mdR5g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", "cpu": [ "arm64" ], @@ -778,9 +779,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.72.tgz", - "integrity": "sha512-x1zKtWVSnf+yLETHdSDAFJ1w6bctS/V2NP0wskTTBKkC+c/AmI2Dl+ZMIW11gF6rilBibrIzBeXJKPzV0GMWGA==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", "cpu": [ "x64" ], @@ -794,9 +795,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.72.tgz", - "integrity": "sha512-Ef6HMF+TBS+lqBNpcUj2D17ODJrbgevXaVPtr2nQFCao5IvoEhVMdmVwWk5YiI+GcgbAkg5AF3LiU47RoSY5yg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", "cpu": [ "arm" ], @@ -810,9 +811,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.72.tgz", - "integrity": "sha512-i1tWu+Li1Z6G4t+ckT38JwuB/cAAREV6H8VD3dip2yTYU+qnLz6kG4i+whm+SEQb1e4vk3xA1lKnjYx3jlOy8g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", "cpu": [ "arm64" ], @@ -826,9 +827,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.72.tgz", - "integrity": "sha512-Mu+2hHZAT9SdrjiRtCxMD/Unac8vqVxF/p+Tvjb5sN1NZkLGu+l7WIfrug8aeX150OwrYgAvsR4mhrm0BZvLxg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", "cpu": [ "arm64" ], @@ -842,9 +843,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.72.tgz", - "integrity": "sha512-xBPG/ImL58I4Ep6VM+sCrpwl8rE/8e7Dt9U7zzggNvYHrWD13vIF3q5L2/N9VxdBMh1pee6dBC/VcaXLYccZNQ==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", "cpu": [ "riscv64" ], @@ -858,9 +859,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.72.tgz", - "integrity": "sha512-jkC8L+QovHpzQrw+Jm1IUqxgLV5QB1hJ1cR8iYzxNRd0TOF7YfxLaIGxvd/ReRi9r48JT6PL7z2IGT7TqK8T4w==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", "cpu": [ "x64" ], @@ -874,9 +875,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.72.tgz", - "integrity": "sha512-PwPdPmHgJYnTMUr8Gff80eRVdpGjwrxueIqw+7v4aeFxbQjmQ+paa2xaGedFtkvdS2Dn5z8a0mVlrlbSfec+1Q==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", "cpu": [ "x64" ], @@ -890,9 +891,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.72.tgz", - "integrity": "sha512-hZhXJZZ/2ZjkAoOtyGUs3Mx6jA4o9ESbc5bk+NKYO6thZRvRNA7rqvT9WF9pZK0xcRK5EyWRymv8fCzqmSVEzg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", "cpu": [ "x64" ], @@ -1104,6 +1105,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.10.8", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.8.tgz", @@ -6983,15 +7000,15 @@ } }, "node_modules/pdfjs-dist": { - "version": "5.3.31", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.31.tgz", - "integrity": "sha512-EhPdIjNX0fcdwYQO+e3BAAJPXt+XI29TZWC7COhIXs/K0JHcUt1Gdz1ITpebTwVMFiLsukdUZ3u0oTO7jij+VA==", + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", "license": "Apache-2.0", "engines": { "node": ">=20.16.0 || >=22.3.0" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.67" + "@napi-rs/canvas": "^0.1.80" } }, "node_modules/pend": { @@ -7040,6 +7057,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", From 6a858bfaae17fb4e0e9ff222da8e0828be25cc11 Mon Sep 17 00:00:00 2001 From: LucasRossignon Date: Sun, 26 Oct 2025 12:20:03 +0100 Subject: [PATCH 02/87] feat: reset password --- middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index ebbd798..019ef2a 100644 --- a/middleware.ts +++ b/middleware.ts @@ -51,6 +51,6 @@ export function middleware(req: NextRequest) { export const config = { matcher: [ - "/((?!api|static|.*\\..*|_next|auth).*)", // /api, /static, file extensions, /_next, and /auth + "/((?!api|static|.*\\..*|_next|auth|reset-password|shared).*)", // /api, /static, file extensions, /_next, /auth, /reset-password, and /shared ] }; From 0cb41ae36dd005532d76d65e4144e2d5339cbc40 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 07:41:12 +0100 Subject: [PATCH 03/87] fix: improve reset password UX and design consistency - Add visual confirmation after sending password reset email - Unify reset password page design with auth pages - Use consistent blue gradient theme and layout - Fix ESLint errors (unused imports and variables) - Remove duplicate temporary files --- app/(auth)/reset-password/page.tsx | 65 ++- components/auth/reset-password-form.tsx | 3 +- .../auth/reset-password-with-token-form.tsx | 373 ++++++++++-------- 3 files changed, 258 insertions(+), 183 deletions(-) diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx index 1ed641b..e3a9b8d 100644 --- a/app/(auth)/reset-password/page.tsx +++ b/app/(auth)/reset-password/page.tsx @@ -1,31 +1,56 @@ "use client"; +import Image from "next/image"; import { Suspense } from "react"; +import { Card, CardContent } from "@/components/ui/card"; import ResetPasswordForm from "@/components/auth/reset-password-with-token-form"; function ResetPasswordContent() { - return ( -
-
-
- + return ( +
+
+ + + {/* Header */} +
+
+
+ Logo Edukai +
+

+ Edukai +

+
+
+ + {/* Reset Password Form */} + +
+
+
-
-
- ); + ); } export default function ResetPasswordPage() { - return ( - -
-
-

Chargement...

-
-
- }> - -
- ); + return ( + +
+
+

Chargement...

+
+
+ } + > + +
+ ); } \ No newline at end of file diff --git a/components/auth/reset-password-form.tsx b/components/auth/reset-password-form.tsx index 4ec449d..24c362c 100644 --- a/components/auth/reset-password-form.tsx +++ b/components/auth/reset-password-form.tsx @@ -20,7 +20,6 @@ export interface ResetPasswordFormProps { } export function ResetPasswordForm({ - onSuccess, onError, onBack, }: ResetPasswordFormProps) { @@ -45,7 +44,7 @@ export function ResetPasswordForm({ if (result.success) { setIsSubmitted(true); - onSuccess?.(); + // Don't call onSuccess() immediately - let user see the confirmation } else { const errorMessage = result.error || "Une erreur est survenue"; setError("root", { message: errorMessage }); diff --git a/components/auth/reset-password-with-token-form.tsx b/components/auth/reset-password-with-token-form.tsx index 1b429ac..74bd814 100644 --- a/components/auth/reset-password-with-token-form.tsx +++ b/components/auth/reset-password-with-token-form.tsx @@ -5,12 +5,9 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { Eye, EyeOff, KeyRound, CheckCircle2, XCircle, Loader2 } from "lucide-react"; +import { Eye, EyeOff, CheckCircle, XCircle, ArrowLeft, Lock } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { FormErrorAlert } from "@/components/ui/form-alert"; import { verifyResetTokenAction, resetPasswordWithTokenAction } from "@/lib/actions/auth"; import { getPasswordRequirements } from "@/lib/schemas/auth"; import { showToast } from "@/lib/toast"; @@ -77,7 +74,7 @@ export default function ResetPasswordWithTokenForm() { } else { setError(response.error || "Token invalide ou expiré"); } - } catch (err) { + } catch { setError("Une erreur est survenue lors de la vérification du token"); } finally { setIsVerifying(false); @@ -110,7 +107,7 @@ export default function ResetPasswordWithTokenForm() { } else { setError(response.error || "Une erreur est survenue"); } - } catch (err) { + } catch { setError("Une erreur est survenue lors de la réinitialisation"); } finally { setIsLoading(false); @@ -120,195 +117,249 @@ export default function ResetPasswordWithTokenForm() { // Loading state while verifying token if (isVerifying) { return ( - - - -

Vérification du lien de réinitialisation...

-
-
+
+
+
+

Vérification du lien de réinitialisation...

+
+
); } // Invalid token state if (!isTokenValid) { return ( - - - - Lien invalide ou expiré - +
+ {/* Error Header */} +
+
+ +
+

+ Lien invalide ou expiré +

+

{error || "Le lien de réinitialisation est invalide ou a expiré."} - - - -

+

+

Les liens de réinitialisation expirent après 30 minutes pour des raisons de sécurité.

- - -
+ + {/* Actions */} +
+ + - + + + - - + + +
+
); } // Success state if (isSuccess) { return ( - - - - Mot de passe réinitialisé ! - +
+ {/* Success Header */} +
+
+ +
+

+ Mot de passe réinitialisé ! +

+

Votre mot de passe a été modifié avec succès. - - - -

+

+

Vous allez être redirigé vers la page de connexion...

- - -
+ + {/* Action */} + + - - + +
); } // Reset form return ( - - - - Créer un nouveau mot de passe - - {maskedEmail && ( - - Réinitialisation pour : {maskedEmail} - - )} - - -
- - {error && } +
+ {/* Header */} +
+

+ Créer un nouveau mot de passe +

+ {maskedEmail && ( +

+ Réinitialisation pour : {maskedEmail} +

+ )} +
- {/* New Password */} -
- -
- - -
- {errors.newPassword && ( -

{errors.newPassword.message}

- )} + {/* Form */} + + {/* Error Display */} + {error && ( +
+

+ + {error} +

+ )} - {/* Password Requirements */} - {newPassword && ( -
-

Exigences du mot de passe :

-
- {passwordRequirements.map((req, index) => ( -
- {req.met ? ( - - ) : ( - - )} - - {req.label} - -
- ))} -
-
+ {/* New Password Field */} +
+ +
+ + +
+ {errors.newPassword && ( +

+ + {errors.newPassword.message} +

)} +
- {/* Confirm Password */} -
- -
- - + {/* Password Requirements */} + {newPassword && ( +
+

+ Exigences du mot de passe : +

+
+ {passwordRequirements.map((req, index) => ( +
+ {req.met ? ( + + ) : ( + + )} + + {req.label} + +
+ ))}
- {errors.confirmPassword && ( -

{errors.confirmPassword.message}

- )}
- - - - +
+ {errors.confirmPassword && ( +

+ + {errors.confirmPassword.message} +

+ )} +
+ + {/* Submit Button */} + + + {/* Cancel Button */} + + - + - +
); } \ No newline at end of file From 68624d3204e0e19c3079d871943582a5584452a5 Mon Sep 17 00:00:00 2001 From: LucasRossignon Date: Sun, 26 Oct 2025 10:43:48 +0100 Subject: [PATCH 04/87] feat: system share course --- app/(dashboard)/library/page.tsx | 3 + app/shared/course/[shareToken]/page.tsx | 303 ++++++++ components/course/Header.tsx | 10 + components/course/ShareCourseDialog.tsx | 207 ++++++ .../sections/summary-sheets/SummarySheets.tsx | 4 + components/library/CourseCard.tsx | 27 +- package.json | 1 + pnpm-lock.yaml | 673 ++++++++++++++++++ services/course.ts | 67 ++ 9 files changed, 1286 insertions(+), 9 deletions(-) create mode 100644 app/shared/course/[shareToken]/page.tsx create mode 100644 components/course/ShareCourseDialog.tsx diff --git a/app/(dashboard)/library/page.tsx b/app/(dashboard)/library/page.tsx index 5f398e2..edd2806 100644 --- a/app/(dashboard)/library/page.tsx +++ b/app/(dashboard)/library/page.tsx @@ -32,6 +32,7 @@ interface ExtendedCourseData { quizzes: string[]; exams: string[]; summarySheets: unknown[]; + shareToken?: string; } // Type for API response that might have different property names @@ -47,6 +48,7 @@ type ApiCourseData = { quizzes: string[]; exams: string[]; summarySheets: unknown[]; + shareToken?: string; }; export default function LibraryPage() { @@ -350,6 +352,7 @@ export default function LibraryPage() { author: course.author, createdAt: course.createdAt, isPublished: course.isPublished, + shareToken: course.shareToken, }))} isLoading={false} /> diff --git a/app/shared/course/[shareToken]/page.tsx b/app/shared/course/[shareToken]/page.tsx new file mode 100644 index 0000000..133ddf0 --- /dev/null +++ b/app/shared/course/[shareToken]/page.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useCourseService } from "@/services"; +import { + ArrowLeft, + BookmarkPlus, + BookOpen, + Calendar, + FileText, + Loader2, + Share2, + User, + XCircle, +} from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +interface SharedCourse { + _id: string; + title: string; + subject: string; + level: string; + author: { + firstName: string; + lastName: string; + username: string; + }; + createdAt: string; + summarySheets: any[]; + quizzes: any[]; + exams: any[]; +} + +export default function SharedCoursePage() { + const params = useParams(); + const router = useRouter(); + const shareToken = params.shareToken as string; + + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isAddingToLibrary, setIsAddingToLibrary] = useState(false); + + const { getSharedCourse, duplicateSharedCourse } = useCourseService(); + + useEffect(() => { + async function fetchSharedCourse() { + try { + setLoading(true); + const response = await getSharedCourse(shareToken); + + if (response.status === "success") { + setCourse(response.item); + } else { + setError( + response.message || "Le cours partagé n'a pas été trouvé." + ); + } + } catch (err) { + setError("Une erreur est survenue lors du chargement."); + } finally { + setLoading(false); + } + } + + if (shareToken) { + fetchSharedCourse(); + } + }, [shareToken]); + + const handleAddToLibrary = async () => { + try { + setIsAddingToLibrary(true); + const response = await duplicateSharedCourse(shareToken); + + if (response.status === "success") { + toast.success("Cours ajouté à votre bibliothèque !"); + // Rediriger vers la bibliothèque + setTimeout(() => { + router.push("/library"); + }, 1500); + } else { + if (response.message?.includes("logged in")) { + toast.error( + "Vous devez être connecté pour ajouter ce cours à votre bibliothèque." + ); + // Redirect to login page + setTimeout(() => { + router.push(`/login?redirect=/shared/course/${shareToken}`); + }, 1500); + } else { + toast.error(response.message || "Erreur lors de l'ajout"); + } + } + } catch (error) { + toast.error("Une erreur est survenue"); + } finally { + setIsAddingToLibrary(false); + } + }; + + if (loading) { + return ( +
+
+ +

Chargement du cours...

+
+
+ ); + } + + if (error || !course) { + return ( +
+
+
+ +
+

+ Cours introuvable +

+

+ {error || + "Le cours partagé n'existe pas ou le lien a été révoqué."} +

+ +
+
+ ); + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("fr-FR", { + day: "numeric", + month: "long", + year: "numeric", + }); + }; + + return ( +
+
+ {/* Header */} +
+
+
+
+ +
+
+

+ {course.title} +

+
+ + + Par {course.author.firstName}{" "} + {course.author.lastName} + + + + {formatDate(course.createdAt)} + +
+
+ + {course.subject} + + + {course.level} + +
+
+
+ +
+ +
+ +
+
+ +
+

+ Cours partagé publiquement +

+

+ Vous pouvez ajouter ce cours à votre + bibliothèque pour accéder à tout son contenu. +

+
+
+
+
+
+ + {/* Content Overview */} +
+ {/* Fiches de révision */} +
+
+
+ +
+

+ Fiches de révision +

+
+

+ {course.summarySheets?.length || 0} +

+

+ {course.summarySheets?.length === 0 + ? "Aucune fiche" + : course.summarySheets?.length === 1 + ? "fiche disponible" + : "fiches disponibles"} +

+
+ + {/* Quizzes */} +
+
+
+ +
+

+ Quizzes +

+
+

+ {course.quizzes?.length || 0} +

+

+ {course.quizzes?.length === 0 + ? "Aucun quiz" + : course.quizzes?.length === 1 + ? "quiz disponible" + : "quizzes disponibles"} +

+
+ + {/* Examens */} +
+
+
+ +
+

+ Examens +

+
+

+ {course.exams?.length || 0} +

+

+ {course.exams?.length === 0 + ? "Aucun examen" + : course.exams?.length === 1 + ? "examen disponible" + : "examens disponibles"} +

+
+
+ + {/* Footer */} +
+

+ Créé avec{" "} + + Edukai + +

+
+
+
+ ); +} diff --git a/components/course/Header.tsx b/components/course/Header.tsx index f9a0704..d4bc7ed 100644 --- a/components/course/Header.tsx +++ b/components/course/Header.tsx @@ -3,6 +3,7 @@ import { OwnerBadge } from "@/components/badge/OwnerBadge"; import { SubjectBadge } from "@/components/badge/SubjectBadge"; import { Button } from "@/components/ui/button"; import { useSession } from "@/hooks/useSession"; +import { ShareCourseDialog } from "./ShareCourseDialog"; import { BicepsFlexed, CircleStop, @@ -12,9 +13,11 @@ import { } from "lucide-react"; type CourseData = { + _id?: string; title?: string; subject?: string; level?: string; + shareToken?: string; }; export type HeaderProps = { @@ -64,6 +67,13 @@ export const Header = ({ {/* Action Buttons - All on same line */}
+
+ +
+ + + + + + + + Partager le cours + + + {isShared + ? "Ce cours est actuellement partagé via le lien ci-dessous." + : "Créez un lien de partage pour ce cours."} + + + +
+ {isShared ? ( + <> +
+ + +
+ +
+

+ ℹ️ Note : Toute + personne ayant ce lien pourra consulter + ce cours avec toutes ses fiches de révision, quizzes et examens. +

+
+ + + + ) : ( + <> +
+

+ Un lien unique sera généré pour partager + ce cours avec d'autres personnes. Ils pourront + voir le contenu et l'ajouter à leur bibliothèque. +

+
+ + + + )} +
+
+
+ + ); +}; diff --git a/components/course/sections/summary-sheets/SummarySheets.tsx b/components/course/sections/summary-sheets/SummarySheets.tsx index 5ba702d..f538763 100644 --- a/components/course/sections/summary-sheets/SummarySheets.tsx +++ b/components/course/sections/summary-sheets/SummarySheets.tsx @@ -124,6 +124,7 @@ export const SummarySheets = ({ } }; + const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("fr-FR", { day: "numeric", @@ -373,6 +374,9 @@ export const SummarySheets = ({ size="sm" variant="ghost" className="h-8 w-8 p-0 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg" + onClick={() => + handleDelete(file) + } > diff --git a/components/library/CourseCard.tsx b/components/library/CourseCard.tsx index 566a7ac..f6e23b6 100644 --- a/components/library/CourseCard.tsx +++ b/components/library/CourseCard.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { formatDate } from "@/lib/date-format"; import { BookOpen, Calendar, User, Eye, GraduationCap } from "lucide-react"; import Link from "next/link"; +import { ShareCourseDialog } from "@/components/course/ShareCourseDialog"; export type CourseCardProps = { id: string; @@ -15,6 +16,7 @@ export type CourseCardProps = { author: string; createdAt: string; isPublished: boolean; + shareToken?: string; }; export const CourseCard = ({ @@ -25,6 +27,7 @@ export const CourseCard = ({ author, createdAt, isPublished, + shareToken, }: CourseCardProps) => { const getSubjectColor = (subject: string) => { const colors = { @@ -132,15 +135,21 @@ export const CourseCard = ({ {isPublished ? "Publié" : "Brouillon"}
- - - +
+ + + + +
diff --git a/package.json b/package.json index d6fd7c0..104e88f 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-dom": "^18", "react-hook-form": "^7.53.2", "react-loading-skeleton": "^3.5.0", + "react-markdown": "^10.1.0", "sonner": "^2.0.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b55e7b..66ec65d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: react-loading-skeleton: specifier: ^3.5.0 version: 3.5.0(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.23)(react@18.3.1) sonner: specifier: ^2.0.1 version: 2.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1248,9 +1251,27 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.1': resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} @@ -1271,6 +1292,12 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1554,6 +1581,9 @@ packages: b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1651,10 +1681,25 @@ packages: resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} engines: {node: '>=10.0.0'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1695,6 +1740,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1772,6 +1820,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1791,9 +1842,16 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + devtools-protocol@0.0.1475386: resolution: {integrity: sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==} @@ -2022,10 +2080,16 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -2224,6 +2288,15 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html2canvas@1.4.1: resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} engines: {node: '>=8.0.0'} @@ -2268,6 +2341,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -2276,6 +2352,12 @@ packages: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2318,6 +2400,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-electron@2.2.2: resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} @@ -2341,6 +2426,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2361,6 +2449,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2492,6 +2584,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2517,10 +2612,97 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2687,6 +2869,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -2817,6 +3002,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -2871,6 +3059,12 @@ packages: peerDependencies: react: '>=16.8.0' + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2936,6 +3130,12 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3068,6 +3268,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3120,6 +3323,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3136,6 +3342,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.18: + resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==} + + style-to-object@1.0.11: + resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -3218,6 +3430,12 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -3277,6 +3495,24 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unrs-resolver@1.9.2: resolution: {integrity: sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==} @@ -3314,6 +3550,12 @@ packages: utrie@1.0.2: resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + wasm-feature-detect@1.8.0: resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==} @@ -3401,6 +3643,9 @@ packages: zod@3.25.67: resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -4366,8 +4611,28 @@ snapshots: tslib: 2.8.1 optional: true + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json5@0.0.29': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@20.19.1': dependencies: undici-types: 6.21.0 @@ -4389,6 +4654,10 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 20.19.1 @@ -4686,6 +4955,8 @@ snapshots: b4a@1.6.7: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} bare-events@2.6.1: @@ -4777,11 +5048,21 @@ snapshots: svg-pathdata: 6.0.3 optional: true + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4834,6 +5115,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@4.1.1: {} concat-map@0.0.1: {} @@ -4900,6 +5183,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -4922,8 +5209,14 @@ snapshots: delayed-stream@1.0.0: {} + dequal@2.0.3: {} + detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + devtools-protocol@0.0.1475386: {} didyoumean@1.2.2: {} @@ -5318,8 +5611,12 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + esutils@2.0.3: {} + extend@3.0.2: {} + extract-zip@2.0.1: dependencies: debug: 4.4.1 @@ -5536,6 +5833,32 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.18 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-url-attributes@3.0.1: {} + html2canvas@1.4.1: dependencies: css-line-break: 2.1.0 @@ -5583,6 +5906,8 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -5591,6 +5916,13 @@ snapshots: ip-address@10.0.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -5641,6 +5973,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-electron@2.2.2: {} is-extglob@2.1.1: {} @@ -5662,6 +5996,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -5675,6 +6011,8 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -5820,6 +6158,8 @@ snapshots: lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5836,8 +6176,230 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -6015,6 +6577,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -6121,6 +6693,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -6200,6 +6774,24 @@ snapshots: dependencies: react: 18.3.1 + react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.23 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): dependencies: react: 18.3.1 @@ -6282,6 +6874,23 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + require-directory@2.1.1: {} resolve-from@4.0.0: {} @@ -6427,6 +7036,8 @@ snapshots: source-map@0.6.1: optional: true + space-separated-tokens@2.0.2: {} + stable-hash@0.0.5: {} stackblur-canvas@2.7.0: @@ -6512,6 +7123,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -6524,6 +7140,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.18: + dependencies: + style-to-object: 1.0.11 + + style-to-object@1.0.11: + dependencies: + inline-style-parser: 0.2.4 + styled-jsx@5.1.1(react@18.3.1): dependencies: client-only: 0.0.1 @@ -6643,6 +7267,10 @@ snapshots: tr46@0.0.3: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -6717,6 +7345,39 @@ snapshots: undici-types@6.21.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unrs-resolver@1.9.2: dependencies: napi-postinstall: 0.2.4 @@ -6770,6 +7431,16 @@ snapshots: dependencies: base64-arraybuffer: 1.0.2 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + wasm-feature-detect@1.8.0: {} webidl-conversions@3.0.1: {} @@ -6868,3 +7539,5 @@ snapshots: zlibjs@0.3.1: {} zod@3.25.67: {} + + zwitch@2.0.4: {} diff --git a/services/course.ts b/services/course.ts index 2ddf352..9f485a9 100644 --- a/services/course.ts +++ b/services/course.ts @@ -45,6 +45,9 @@ export interface CourseService { description: string, date: Date ) => Promise<{ message: string } | null>; + toggleShare: (courseId: string) => Promise; + getSharedCourse: (shareToken: string) => Promise; + duplicateSharedCourse: (shareToken: string) => Promise; } export function useCourseService() { @@ -265,6 +268,67 @@ export function useCourseService() { } }; + const toggleShare = async (courseId: string) => { + try { + const response = await axios.post( + `${apiUrl}/courses/${courseId}/share`, + {}, + { withCredentials: true } + ); + + return response.data; + } catch (error: any) { + if (error?.response?.data) { + return error.response.data; + } + + return { + status: "failure", + message: "Une erreur est survenue lors du partage du cours.", + }; + } + }; + + const getSharedCourse = async (shareToken: string) => { + try { + const response = await axios.get( + `${apiUrl}/courses/shared/${shareToken}` + ); + + return response.data; + } catch (error: any) { + if (error?.response?.data) { + return error.response.data; + } + + return { + status: "failure", + message: "Le cours partagé n'a pas été trouvé ou le lien a été révoqué.", + }; + } + }; + + const duplicateSharedCourse = async (shareToken: string) => { + try { + const response = await axios.post( + `${apiUrl}/courses/shared/${shareToken}/duplicate`, + {}, + { withCredentials: true } + ); + + return response.data; + } catch (error: any) { + if (error?.response?.data) { + return error.response.data; + } + + return { + status: "failure", + message: "Une erreur est survenue lors de l'ajout du cours à votre bibliothèque.", + }; + } + }; + return { createCourse, getCourseById, @@ -278,5 +342,8 @@ export function useCourseService() { getExamById, updateExamById, deleteExamById, + toggleShare, + getSharedCourse, + duplicateSharedCourse, }; } From 95d2541aa6013ffbcb5b281eb3e3a9463cc95506 Mon Sep 17 00:00:00 2001 From: LucasRossignon Date: Sun, 26 Oct 2025 11:01:59 +0100 Subject: [PATCH 05/87] fix: error linter --- app/shared/course/[shareToken]/page.tsx | 15 ++++++++------- components/course/ShareCourseDialog.tsx | 9 ++++----- components/course/sections/Files/FileCard.tsx | 5 +---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/shared/course/[shareToken]/page.tsx b/app/shared/course/[shareToken]/page.tsx index 133ddf0..0077a05 100644 --- a/app/shared/course/[shareToken]/page.tsx +++ b/app/shared/course/[shareToken]/page.tsx @@ -28,9 +28,9 @@ interface SharedCourse { username: string; }; createdAt: string; - summarySheets: any[]; - quizzes: any[]; - exams: any[]; + summarySheets: unknown[]; + quizzes: unknown[]; + exams: unknown[]; } export default function SharedCoursePage() { @@ -58,7 +58,7 @@ export default function SharedCoursePage() { response.message || "Le cours partagé n'a pas été trouvé." ); } - } catch (err) { + } catch { setError("Une erreur est survenue lors du chargement."); } finally { setLoading(false); @@ -68,6 +68,7 @@ export default function SharedCoursePage() { if (shareToken) { fetchSharedCourse(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [shareToken]); const handleAddToLibrary = async () => { @@ -94,7 +95,7 @@ export default function SharedCoursePage() { toast.error(response.message || "Erreur lors de l'ajout"); } } - } catch (error) { + } catch { toast.error("Une erreur est survenue"); } finally { setIsAddingToLibrary(false); @@ -124,11 +125,11 @@ export default function SharedCoursePage() {

{error || - "Le cours partagé n'existe pas ou le lien a été révoqué."} + "Le cours partagé n'existe pas ou le lien a été révoqué."}

diff --git a/components/course/ShareCourseDialog.tsx b/components/course/ShareCourseDialog.tsx index d7bc8ec..44d3a40 100644 --- a/components/course/ShareCourseDialog.tsx +++ b/components/course/ShareCourseDialog.tsx @@ -65,9 +65,8 @@ export const ShareCourseDialog = ({ "Erreur lors du partage du cours" ); } - } catch (error) { + } catch { toast.error("Une erreur est survenue"); - console.error("Error toggling share:", error); } finally { setIsLoading(false); } @@ -84,7 +83,7 @@ export const ShareCourseDialog = ({ setTimeout(() => { setCopied(false); }, 2000); - } catch (error) { + } catch { toast.error("Erreur lors de la copie du lien"); } }; @@ -182,8 +181,8 @@ export const ShareCourseDialog = ({

Un lien unique sera généré pour partager - ce cours avec d'autres personnes. Ils pourront - voir le contenu et l'ajouter à leur bibliothèque. + ce cours avec d'autres personnes. Ils pourront + voir le contenu et l'ajouter à leur bibliothèque.

diff --git a/components/course/sections/Files/FileCard.tsx b/components/course/sections/Files/FileCard.tsx index 870cb23..763bb77 100644 --- a/components/course/sections/Files/FileCard.tsx +++ b/components/course/sections/Files/FileCard.tsx @@ -19,10 +19,7 @@ const getFileIcon = (contentType: string, isZip?: boolean) => { case "archive": return ; case "image": - { - /* eslint-disable-next-line jsx-a11y/alt-text */ - } - /* eslint-disable-next-line @next/next/no-img-element */ + /* eslint-disable-next-line jsx-a11y/alt-text */ return ; case "text": return ; From e605ae9494bc4134865344ccc46766c7cc6483a8 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 07:54:55 +0100 Subject: [PATCH 06/87] feat: enhance ShareCourseDialog UI and improve user experience --- components/course/ShareCourseDialog.tsx | 165 ++++++++++++------------ 1 file changed, 84 insertions(+), 81 deletions(-) diff --git a/components/course/ShareCourseDialog.tsx b/components/course/ShareCourseDialog.tsx index 44d3a40..012059d 100644 --- a/components/course/ShareCourseDialog.tsx +++ b/components/course/ShareCourseDialog.tsx @@ -114,90 +114,93 @@ export const ShareCourseDialog = ({ - - - - - Partager le cours - - - {isShared - ? "Ce cours est actuellement partagé via le lien ci-dessous." - : "Créez un lien de partage pour ce cours."} - - - -
- {isShared ? ( - <> -
- + +
+ +
+ +
+ + Partager le cours + + + {isShared + ? "Ce cours est actuellement partagé via le lien ci-dessous." + : "Créez un lien de partage pour ce cours."} + +
+ +
+ {isShared ? ( + <> +
+ + +
+ +
+

+ ℹ️ Note : Toute + personne ayant ce lien pourra consulter + ce cours avec toutes ses fiches de révision, quizzes et examens. +

+
+ + + + ) : ( + <> +
+

+ Un lien unique sera généré pour partager + ce cours avec d'autres personnes. Ils pourront + voir le contenu et l'ajouter à leur bibliothèque. +

+
+ -
- -
-

- ℹ️ Note : Toute - personne ayant ce lien pourra consulter - ce cours avec toutes ses fiches de révision, quizzes et examens. -

-
- - - - ) : ( - <> -
-

- Un lien unique sera généré pour partager - ce cours avec d'autres personnes. Ils pourront - voir le contenu et l'ajouter à leur bibliothèque. -

-
- - - - )} + + )} +
From 7719e34a5a2a2962581dc024a802e6381fddecad Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 09:16:24 +0100 Subject: [PATCH 07/87] feat: add course settings page and public content components - Add dedicated settings page for course visibility management - Add PublicCourseCard and PublicSheetCard components for Club Edukai - Implement slider toggle for public/private course visibility - Add comprehensive info box explaining public visibility features --- .../library/[id]/settings/page.tsx | 226 ++++++++++++++++++ components/club/PublicCourseCard.tsx | 145 +++++++++++ components/club/PublicSheetCard.tsx | 95 ++++++++ 3 files changed, 466 insertions(+) create mode 100644 app/(dashboard)/library/[id]/settings/page.tsx create mode 100644 components/club/PublicCourseCard.tsx create mode 100644 components/club/PublicSheetCard.tsx diff --git a/app/(dashboard)/library/[id]/settings/page.tsx b/app/(dashboard)/library/[id]/settings/page.tsx new file mode 100644 index 0000000..ff28998 --- /dev/null +++ b/app/(dashboard)/library/[id]/settings/page.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Header } from "@/components/course/Header"; +import { LoadingState } from "@/components/course/components"; +import { useCourseLogic } from "@/hooks/course"; +import { useCourseService } from "@/services"; +import { ArrowLeft, Globe, Lock, Loader2, Save } from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +export default function CourseSettingsPage() { + const params = useParams(); + const router = useRouter(); + const courseId = params.id as string; + + const { courseData, setSelectedTab, selectedTab } = useCourseLogic(); + const [saving, setSaving] = useState(false); + const [isShared, setIsShared] = useState(false); + + const { toggleShare } = useCourseService(); + + // Initialize isShared from courseData + useEffect(() => { + if (courseData) { + setIsShared(courseData.isShared || false); + + // Redirect if not owner + if (courseData.isOwner === false) { + toast.error("Vous n'avez pas accès à cette page"); + router.push(`/library/${courseId}`); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [courseData]); + + const handleSave = async () => { + if (!courseData) return; + + // Only save if changed + if (isShared === courseData.isShared) { + toast.info("Aucune modification à enregistrer"); + return; + } + + setSaving(true); + try { + const response = await toggleShare(courseId); + + if (response.status === "success") { + toast.success( + isShared + ? "Cours rendu public avec succès !" + : "Cours rendu privé avec succès !" + ); + // Reload the page to refresh course data + window.location.reload(); + } else { + // Revert on error + setIsShared(courseData.isShared || false); + toast.error( + response.message || "Erreur lors de la modification" + ); + } + } catch (error) { + console.error("Error toggling share:", error); + setIsShared(courseData.isShared || false); + toast.error("Une erreur est survenue"); + } finally { + setSaving(false); + } + }; + + if (!courseData) { + return ; + } + + const hasChanges = isShared !== courseData.isShared; + + return ( +
+ {/* Course Header */} +
+ + {/* Back Button */} +
+ +
+ + {/* Settings Card */} +
+
+ {/* Settings Title */} +
+

+ Paramètres du cours +

+
+ {/* Visibility Section */} +
+
+
+

+ Visibilité du cours +

+

+ Contrôlez qui peut voir votre cours +

+
+
+ + {/* Toggle Switch */} +
+
+
+ {isShared ? ( + + ) : ( + + )} +
+
+

+ {isShared ? "Public" : "Privé"} +

+

+ {isShared + ? "Visible dans le Club Edukai" + : "Uniquement visible par vous"} +

+
+
+ + {/* Custom Slider */} + +
+ + {/* Info Box */} +
+

+ ℹ️ À propos de la visibilité publique +

+
    +
  • + Les cours publics apparaissent dans le Club + Edukai +
  • +
  • + Les utilisateurs peuvent consulter le cours + en lecture seule +
  • +
  • + Vous restez le seul propriétaire et pouvez + modifier le cours +
  • +
  • + Vous pouvez repasser en privé à tout moment +
  • +
+
+
+ + {/* Save Button */} + {hasChanges && ( +
+ + +
+ )} +
+
+
+ ); +} diff --git a/components/club/PublicCourseCard.tsx b/components/club/PublicCourseCard.tsx new file mode 100644 index 0000000..7e166d3 --- /dev/null +++ b/components/club/PublicCourseCard.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { formatDate } from "@/lib/date-format"; +import { BookOpen, Calendar, User, Eye, GraduationCap } from "lucide-react"; +import Link from "next/link"; + +export type PublicCourseCardProps = { + id: string; + title: string; + subject: string; + level: string; + author: { + firstName: string; + lastName: string; + username: string; + }; + createdAt: string; +}; + +export const PublicCourseCard = ({ + id, + title, + subject, + level, + author, + createdAt, +}: PublicCourseCardProps) => { + const getSubjectColor = (subject: string) => { + const colors = { + Mathématiques: "bg-blue-100 text-blue-800 border-blue-200", + Physique: "bg-purple-100 text-purple-800 border-purple-200", + Chimie: "bg-green-100 text-green-800 border-green-200", + Biologie: "bg-emerald-100 text-emerald-800 border-emerald-200", + Histoire: "bg-orange-100 text-orange-800 border-orange-200", + Géographie: "bg-yellow-100 text-yellow-800 border-yellow-200", + Français: "bg-red-100 text-red-800 border-red-200", + Anglais: "bg-indigo-100 text-indigo-800 border-indigo-200", + Espagnol: "bg-pink-100 text-pink-800 border-pink-200", + Philosophie: "bg-gray-100 text-gray-800 border-gray-200", + }; + return ( + colors[subject as keyof typeof colors] || + "bg-gray-100 text-gray-800 border-gray-200" + ); + }; + + const getLevelColor = (level: string) => { + const colors = { + Sixième: "bg-emerald-50 text-emerald-700", + Cinquième: "bg-teal-50 text-teal-700", + Quatrième: "bg-cyan-50 text-cyan-700", + Troisième: "bg-blue-50 text-blue-700", + Seconde: "bg-indigo-50 text-indigo-700", + Première: "bg-purple-50 text-purple-700", + Terminale: "bg-pink-50 text-pink-700", + TERMIIIIINALE: "bg-pink-50 text-pink-700", + }; + return ( + colors[level as keyof typeof colors] || "bg-gray-50 text-gray-700" + ); + }; + + const authorName = `${author.firstName} ${author.lastName}`; + + return ( + + +
+ {/* Header Section */} +
+
+
+
+ +
+ + {subject} + +
+

+ {title} +

+
+ + + +
+ + {/* Content Section */} +
+
+ + + {level} + +
+
+ + + {authorName} + +
+
+ + + {formatDate( + new Date(createdAt).toLocaleDateString( + "fr-FR" + ) + )} + +
+
+ + {/* Action Section */} +
+ + + +
+
+
+
+ ); +}; diff --git a/components/club/PublicSheetCard.tsx b/components/club/PublicSheetCard.tsx new file mode 100644 index 0000000..8ec6496 --- /dev/null +++ b/components/club/PublicSheetCard.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { formatDate } from "@/lib/date-format"; +import { FileText, Calendar, User, Eye } from "lucide-react"; +import Link from "next/link"; + +export type PublicSheetCardProps = { + id: string; + title?: string; + author: { + firstName: string; + lastName: string; + username: string; + }; + createdAt: string; +}; + +export const PublicSheetCard = ({ + id, + title, + author, + createdAt, +}: PublicSheetCardProps) => { + const authorName = `${author.firstName} ${author.lastName}`; + const sheetTitle = title || `Fiche de révision`; + + return ( + + +
+ {/* Header Section */} +
+
+
+
+ +
+ + Fiche de révision + +
+

+ {sheetTitle} +

+
+ + + +
+ + {/* Content Section */} +
+
+ + + {authorName} + +
+
+ + + {formatDate( + new Date(createdAt).toLocaleDateString( + "fr-FR" + ) + )} + +
+
+ + {/* Action Section */} +
+ + + +
+
+
+
+ ); +}; From 3a40c3247a928256123b8dbfdd2c9c74b720a776 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 09:17:02 +0100 Subject: [PATCH 08/87] feat: implement Club Edukai with search and filters - Add tabbed interface for public courses and summary sheets - Implement comprehensive search functionality across title and author - Add subject and level filters with dynamic dropdowns - Add filter counter badges and reset functionality - Integrate getPublicCourses and getPublicSheets API endpoints --- app/(dashboard)/club-edukai/page.tsx | 323 ++++++++++++++++++++++++++- services/ai/summarySheet.ts | 42 +++- services/course.ts | 35 +-- 3 files changed, 358 insertions(+), 42 deletions(-) diff --git a/app/(dashboard)/club-edukai/page.tsx b/app/(dashboard)/club-edukai/page.tsx index 5fd8f34..b7ac554 100644 --- a/app/(dashboard)/club-edukai/page.tsx +++ b/app/(dashboard)/club-edukai/page.tsx @@ -1,8 +1,124 @@ "use client"; -import { Users, Calendar, Trophy, Star, UserPlus } from "lucide-react"; +import { Users, Calendar, Trophy, Star, UserPlus, BookOpen, FileText, Search, Filter, X } from "lucide-react"; +import { PublicCourseCard } from "@/components/club/PublicCourseCard"; +import { PublicSheetCard } from "@/components/club/PublicSheetCard"; +import { useCourseService, useSummarySheetService } from "@/services"; +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; + +type TabType = "courses" | "sheets"; + +interface PublicCourse { + _id: string; + title: string; + subject: string; + level: string; + author: { + firstName: string; + lastName: string; + username: string; + }; + createdAt: string; +} + +interface PublicSheet { + _id: string; + title?: string; + author: { + firstName: string; + lastName: string; + username: string; + }; + createdAt: string; +} export default function ClubEdukaiPage() { + const [activeTab, setActiveTab] = useState("courses"); + const [courses, setCourses] = useState([]); + const [sheets, setSheets] = useState([]); + const [loadingCourses, setLoadingCourses] = useState(true); + const [loadingSheets, setLoadingSheets] = useState(true); + + // Filters state + const [searchQuery, setSearchQuery] = useState(""); + const [selectedSubject, setSelectedSubject] = useState("all"); + const [selectedLevel, setSelectedLevel] = useState("all"); + const [showFilters, setShowFilters] = useState(false); + + const { getPublicCourses } = useCourseService(); + const { getPublicSheets } = useSummarySheetService(); + + // Extract unique subjects and levels from courses + const subjects = Array.from(new Set(courses.map(c => c.subject))).sort(); + const levels = Array.from(new Set(courses.map(c => c.level))).sort(); + + // Filter courses based on search and filters + const filteredCourses = courses.filter(course => { + const matchesSearch = course.title.toLowerCase().includes(searchQuery.toLowerCase()) || + course.author.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || + course.author.lastName.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesSubject = selectedSubject === "all" || course.subject === selectedSubject; + const matchesLevel = selectedLevel === "all" || course.level === selectedLevel; + + return matchesSearch && matchesSubject && matchesLevel; + }); + + // Filter sheets based on search + const filteredSheets = sheets.filter(sheet => { + const matchesSearch = (sheet.title?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false) || + sheet.author.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || + sheet.author.lastName.toLowerCase().includes(searchQuery.toLowerCase()); + + return matchesSearch; + }); + + // Reset filters + const resetFilters = () => { + setSearchQuery(""); + setSelectedSubject("all"); + setSelectedLevel("all"); + }; + + const hasActiveFilters = searchQuery !== "" || selectedSubject !== "all" || selectedLevel !== "all"; + + useEffect(() => { + async function fetchPublicCourses() { + setLoadingCourses(true); + try { + const response = await getPublicCourses(); + if (response.status === "success") { + setCourses(response.items || []); + } + } catch (error) { + console.error("Error fetching public courses:", error); + } finally { + setLoadingCourses(false); + } + } + + fetchPublicCourses(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + async function fetchPublicSheets() { + setLoadingSheets(true); + try { + const response = await getPublicSheets(); + if (response.status === "success") { + setSheets(response.items || []); + } + } catch (error) { + console.error("Error fetching public sheets:", error); + } finally { + setLoadingSheets(false); + } + } + + fetchPublicSheets(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
{/* Beautiful Header Section */} @@ -124,19 +240,204 @@ export default function ClubEdukaiPage() {
- {/* Content Area - Ready for future content */} + {/* Content Area with Tabs */}
- {/* Future content will go here */} -
-
- - Contenu à venir + {/* Tabs Navigation */} +
+ + +
+ + {/* Search and Filters */} +
+ {/* Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-12 pr-4 py-3 rounded-xl border border-gray-200 bg-white/70 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" + /> +
+ {activeTab === "courses" && ( + + )} + {hasActiveFilters && ( + + )}
-

- Cette section sera bientôt remplie avec du contenu - passionnant sur le Club Edukai. -

+ + {/* Filter Dropdowns (only for courses) */} + {activeTab === "courses" && showFilters && ( +
+
+ + +
+
+ + +
+
+ )} +
+ + {/* Tab Content */} +
+ {activeTab === "courses" && ( +
+ {loadingCourses ? ( +
+ +
+ ) : filteredCourses.length === 0 ? ( +
+
+ + {courses.length === 0 ? "Aucun cours public" : "Aucun résultat"} +
+

+ {courses.length === 0 + ? "Il n'y a pas encore de cours publics disponibles. Revenez plus tard !" + : "Aucun cours ne correspond à vos critères. Essayez de modifier vos filtres."} +

+
+ ) : ( +
+ {filteredCourses.map((course) => ( + + ))} +
+ )} +
+ )} + + {activeTab === "sheets" && ( +
+ {loadingSheets ? ( +
+ +
+ ) : filteredSheets.length === 0 ? ( +
+
+ + {sheets.length === 0 ? "Aucune fiche publique" : "Aucun résultat"} +
+

+ {sheets.length === 0 + ? "Il n'y a pas encore de fiches de révision publiques. Revenez plus tard !" + : "Aucune fiche ne correspond à votre recherche."} +

+
+ ) : ( +
+ {filteredSheets.map((sheet) => ( + + ))} +
+ )} +
+ )}
diff --git a/services/ai/summarySheet.ts b/services/ai/summarySheet.ts index 6c68a74..833a0fc 100644 --- a/services/ai/summarySheet.ts +++ b/services/ai/summarySheet.ts @@ -4,6 +4,8 @@ export interface SummarySheetService { generateSheet: (recognizedText: string[]) => Promise; getSheetById: (sheetId: string) => Promise; deleteSheetById: (sheetId: string) => Promise; + toggleShare: (sheetId: string) => Promise; + getPublicSheets: () => Promise; } export function useSummarySheetService(): SummarySheetService { @@ -66,5 +68,43 @@ export function useSummarySheetService(): SummarySheetService { } }; - return { generateSheet, getSheetById, deleteSheetById }; + const toggleShare = async (sheetId: string) => { + try { + const response = await axios.post( + `${apiUrl}/summary-sheets/${sheetId}/share`, + {}, + { withCredentials: true } + ); + + return response.data; + } catch (error: any) { + if (error?.response?.data) { + return error.response.data; + } + + return { + status: "failure", + message: "Une erreur est survenue lors du partage de la fiche.", + }; + } + }; + + const getPublicSheets = async () => { + try { + const response = await axios.get(`${apiUrl}/summary-sheets/public`); + + return response.data; + } catch (error: any) { + if (error?.response?.data) { + return error.response.data; + } + + return { + status: "failure", + message: "Une erreur est survenue lors de la récupération des fiches publiques.", + }; + } + }; + + return { generateSheet, getSheetById, deleteSheetById, toggleShare, getPublicSheets }; } diff --git a/services/course.ts b/services/course.ts index 9f485a9..f4cd09d 100644 --- a/services/course.ts +++ b/services/course.ts @@ -46,8 +46,7 @@ export interface CourseService { date: Date ) => Promise<{ message: string } | null>; toggleShare: (courseId: string) => Promise; - getSharedCourse: (shareToken: string) => Promise; - duplicateSharedCourse: (shareToken: string) => Promise; + getPublicCourses: () => Promise; } export function useCourseService() { @@ -289,32 +288,9 @@ export function useCourseService() { } }; - const getSharedCourse = async (shareToken: string) => { + const getPublicCourses = async () => { try { - const response = await axios.get( - `${apiUrl}/courses/shared/${shareToken}` - ); - - return response.data; - } catch (error: any) { - if (error?.response?.data) { - return error.response.data; - } - - return { - status: "failure", - message: "Le cours partagé n'a pas été trouvé ou le lien a été révoqué.", - }; - } - }; - - const duplicateSharedCourse = async (shareToken: string) => { - try { - const response = await axios.post( - `${apiUrl}/courses/shared/${shareToken}/duplicate`, - {}, - { withCredentials: true } - ); + const response = await axios.get(`${apiUrl}/courses/public`); return response.data; } catch (error: any) { @@ -324,7 +300,7 @@ export function useCourseService() { return { status: "failure", - message: "Une erreur est survenue lors de l'ajout du cours à votre bibliothèque.", + message: "Une erreur est survenue lors de la récupération des cours publics.", }; } }; @@ -343,7 +319,6 @@ export function useCourseService() { updateExamById, deleteExamById, toggleShare, - getSharedCourse, - duplicateSharedCourse, + getPublicCourses, }; } From ec919b90120fd2eb782e7ce5482ea8be568e26eb Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 09:17:12 +0100 Subject: [PATCH 09/87] refactor: improve course ownership and visibility handling - Add frontend fallback for isOwner verification in Header component - Extend CourseData interface with isOwner, isShared, and author properties - Replace inline toggle with clickable visibility badge in CourseCard - Handle author as both string and object types in library page --- app/(dashboard)/library/page.tsx | 17 +++++++++---- components/course/Header.tsx | 41 +++++++++++++++++++------------ components/library/CourseCard.tsx | 38 ++++++++++++++++++++-------- hooks/useCourse.ts | 9 +++++++ 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/app/(dashboard)/library/page.tsx b/app/(dashboard)/library/page.tsx index edd2806..2239360 100644 --- a/app/(dashboard)/library/page.tsx +++ b/app/(dashboard)/library/page.tsx @@ -32,7 +32,7 @@ interface ExtendedCourseData { quizzes: string[]; exams: string[]; summarySheets: unknown[]; - shareToken?: string; + isShared?: boolean; } // Type for API response that might have different property names @@ -42,13 +42,18 @@ type ApiCourseData = { title: string; subject: string; level: string; - author?: string; + author?: string | { + _id?: string; + username?: string; + firstName?: string; + lastName?: string; + }; isPublished?: boolean; createdAt?: string; quizzes: string[]; exams: string[]; summarySheets: unknown[]; - shareToken?: string; + isShared?: boolean; }; export default function LibraryPage() { @@ -185,7 +190,9 @@ export default function LibraryPage() { (course: ApiCourseData) => ({ ...course, id: course._id || course.id || "", - author: course.author || "Unknown", + author: typeof course.author === 'string' + ? course.author + : (course.author?.username || course.author?.firstName || "Unknown"), isPublished: course.isPublished || false, createdAt: course.createdAt || new Date().toISOString(), }) @@ -352,7 +359,7 @@ export default function LibraryPage() { author: course.author, createdAt: course.createdAt, isPublished: course.isPublished, - shareToken: course.shareToken, + isShared: course.isShared, }))} isLoading={false} /> diff --git a/components/course/Header.tsx b/components/course/Header.tsx index d4bc7ed..be5a462 100644 --- a/components/course/Header.tsx +++ b/components/course/Header.tsx @@ -3,7 +3,6 @@ import { OwnerBadge } from "@/components/badge/OwnerBadge"; import { SubjectBadge } from "@/components/badge/SubjectBadge"; import { Button } from "@/components/ui/button"; import { useSession } from "@/hooks/useSession"; -import { ShareCourseDialog } from "./ShareCourseDialog"; import { BicepsFlexed, CircleStop, @@ -11,13 +10,21 @@ import { Settings, BookOpen, } from "lucide-react"; +import { useRouter } from "next/navigation"; type CourseData = { _id?: string; title?: string; subject?: string; level?: string; - shareToken?: string; + isShared?: boolean; + isOwner?: boolean; + author?: { + _id?: string; + username?: string; + firstName?: string; + lastName?: string; + }; }; export type HeaderProps = { @@ -33,12 +40,18 @@ export const Header = ({ }: HeaderProps) => { const course = courseData as CourseData; const { user } = useSession(); - + const router = useRouter(); + // Get display name: use username to match sidebar const getDisplayName = () => { return user?.username || "Utilisateur"; }; + // Check if user is the owner (frontend fallback if backend isOwner is incorrect) + const isUserOwner = course.isOwner || + (user?.email && course.author?.username === user.email) || + (user?.username && course.author?.username === user.username); + return (
{/* Background Pattern */} @@ -67,13 +80,6 @@ export const Header = ({ {/* Action Buttons - All on same line */}
-
- -
- - + {isUserOwner && ( + + )}
diff --git a/components/library/CourseCard.tsx b/components/library/CourseCard.tsx index f6e23b6..5aa1edc 100644 --- a/components/library/CourseCard.tsx +++ b/components/library/CourseCard.tsx @@ -1,12 +1,12 @@ "use client"; -import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { formatDate } from "@/lib/date-format"; -import { BookOpen, Calendar, User, Eye, GraduationCap } from "lucide-react"; +import { BookOpen, Calendar, Eye, GraduationCap, User, Globe, Lock } from "lucide-react"; import Link from "next/link"; -import { ShareCourseDialog } from "@/components/course/ShareCourseDialog"; +import { useRouter } from "next/navigation"; export type CourseCardProps = { id: string; @@ -16,7 +16,7 @@ export type CourseCardProps = { author: string; createdAt: string; isPublished: boolean; - shareToken?: string; + isShared?: boolean; }; export const CourseCard = ({ @@ -26,9 +26,11 @@ export const CourseCard = ({ level, author, createdAt, - isPublished, - shareToken, + isPublished = true, + isShared = false, }: CourseCardProps) => { + const router = useRouter(); + const getSubjectColor = (subject: string) => { const colors = { Mathématiques: "bg-blue-100 text-blue-800 border-blue-200", @@ -136,10 +138,26 @@ export const CourseCard = ({
- + -
- - ); - } - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString("fr-FR", { - day: "numeric", - month: "long", - year: "numeric", - }); - }; - - return ( -
-
- {/* Header */} -
-
-
-
- -
-
-

- {course.title} -

-
- - - Par {course.author.firstName}{" "} - {course.author.lastName} - - - - {formatDate(course.createdAt)} - -
-
- - {course.subject} - - - {course.level} - -
-
-
- -
- -
- -
-
- -
-

- Cours partagé publiquement -

-

- Vous pouvez ajouter ce cours à votre - bibliothèque pour accéder à tout son contenu. -

-
-
-
-
-
- - {/* Content Overview */} -
- {/* Fiches de révision */} -
-
-
- -
-

- Fiches de révision -

-
-

- {course.summarySheets?.length || 0} -

-

- {course.summarySheets?.length === 0 - ? "Aucune fiche" - : course.summarySheets?.length === 1 - ? "fiche disponible" - : "fiches disponibles"} -

-
- - {/* Quizzes */} -
-
-
- -
-

- Quizzes -

-
-

- {course.quizzes?.length || 0} -

-

- {course.quizzes?.length === 0 - ? "Aucun quiz" - : course.quizzes?.length === 1 - ? "quiz disponible" - : "quizzes disponibles"} -

-
- - {/* Examens */} -
-
-
- -
-

- Examens -

-
-

- {course.exams?.length || 0} -

-

- {course.exams?.length === 0 - ? "Aucun examen" - : course.exams?.length === 1 - ? "examen disponible" - : "examens disponibles"} -

-
-
- - {/* Footer */} -
-

- Créé avec{" "} - - Edukai - -

-
-
-
- ); -} diff --git a/components/course/ShareCourseDialog.tsx b/components/course/ShareCourseDialog.tsx deleted file mode 100644 index 012059d..0000000 --- a/components/course/ShareCourseDialog.tsx +++ /dev/null @@ -1,209 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { useCourseService } from "@/services"; -import { Check, Copy, Link, Share2, XCircle } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; - -interface ShareCourseDialogProps { - courseId: string; - shareToken?: string; - onShareToggled?: (courseId: string, newShareToken?: string) => void; -} - -export const ShareCourseDialog = ({ - courseId, - shareToken, - onShareToggled, -}: ShareCourseDialogProps) => { - const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [copied, setCopied] = useState(false); - const [localShareToken, setLocalShareToken] = useState(shareToken); - - const { toggleShare } = useCourseService(); - - const isShared = !!localShareToken; - const shareUrl = localShareToken - ? `${window.location.origin}/shared/course/${localShareToken}` - : ""; - - const handleToggleShare = async () => { - setIsLoading(true); - try { - const response = await toggleShare(courseId); - - if (response.status === "success") { - const newToken = response.item?.shareToken; - setLocalShareToken(newToken); - - if (onShareToggled) { - onShareToggled(courseId, newToken); - } - - toast.success( - newToken - ? "Lien de partage créé avec succès !" - : "Lien de partage révoqué." - ); - - if (!newToken) { - setIsOpen(false); - } - } else { - toast.error( - response.message || - "Erreur lors du partage du cours" - ); - } - } catch { - toast.error("Une erreur est survenue"); - } finally { - setIsLoading(false); - } - }; - - const handleCopyLink = async () => { - if (!shareUrl) return; - - try { - await navigator.clipboard.writeText(shareUrl); - setCopied(true); - toast.success("Lien copié dans le presse-papier !"); - - setTimeout(() => { - setCopied(false); - }, 2000); - } catch { - toast.error("Erreur lors de la copie du lien"); - } - }; - - return ( - <> - - - - -
- -
- -
- - Partager le cours - - - {isShared - ? "Ce cours est actuellement partagé via le lien ci-dessous." - : "Créez un lien de partage pour ce cours."} - -
- -
- {isShared ? ( - <> -
- - -
- -
-

- ℹ️ Note : Toute - personne ayant ce lien pourra consulter - ce cours avec toutes ses fiches de révision, quizzes et examens. -

-
- - - - ) : ( - <> -
-

- Un lien unique sera généré pour partager - ce cours avec d'autres personnes. Ils pourront - voir le contenu et l'ajouter à leur bibliothèque. -

-
- - - - )} -
-
-
-
- - ); -}; From 0fec84dbdd697223697abb4982e7ca60c97adcc7 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 15:41:32 +0100 Subject: [PATCH 11/87] feat: add Visibility enum for content visibility management Introduce centralized Visibility enum with PUBLIC and PRIVATE constants to replace hardcoded strings throughout the codebase. This improves type safety and maintainability. --- lib/types/visibility.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/types/visibility.ts diff --git a/lib/types/visibility.ts b/lib/types/visibility.ts new file mode 100644 index 0000000..51c090b --- /dev/null +++ b/lib/types/visibility.ts @@ -0,0 +1,12 @@ +/** + * Enum for content visibility + */ +export enum Visibility { + PUBLIC = "public", + PRIVATE = "private", +} + +/** + * Type for visibility values + */ +export type VisibilityType = `${Visibility}`; From 090d8803dd851d1704a2a291a61a614be94f8e47 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 15:41:44 +0100 Subject: [PATCH 12/87] feat: add reset password page Add reset password page with form component, following the same design pattern as other auth pages with gradient background and card layout. --- app/(auth)/reset-password/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx index e3a9b8d..322ed68 100644 --- a/app/(auth)/reset-password/page.tsx +++ b/app/(auth)/reset-password/page.tsx @@ -53,4 +53,4 @@ export default function ResetPasswordPage() { ); -} \ No newline at end of file +} From c40e1c9ef895ad481585d75f0962048e5a907c17 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 15:41:56 +0100 Subject: [PATCH 13/87] refactor: migrate from isShared to visibility enum pattern Replace boolean isShared field with visibility field using Visibility enum across all course-related types and components. Update CourseData interface, CourseCard, settings page, and Club Edukai to use the new visibility pattern aligned with backend API. - Update SummarySheetData to include visibility, isOwner, and title fields - Replace isShared checks with visibility === Visibility.PUBLIC - Simplify Club Edukai to show only courses (removed summary sheets tab) --- app/(dashboard)/club-edukai/page.tsx | 233 +++++------------- .../library/[id]/settings/page.tsx | 34 +-- app/(dashboard)/library/page.tsx | 11 +- components/course/Header.tsx | 3 +- components/library/CourseCard.tsx | 10 +- hooks/useCourse.ts | 8 +- lib/types/library.ts | 5 + 7 files changed, 97 insertions(+), 207 deletions(-) diff --git a/app/(dashboard)/club-edukai/page.tsx b/app/(dashboard)/club-edukai/page.tsx index b7ac554..8c83ac2 100644 --- a/app/(dashboard)/club-edukai/page.tsx +++ b/app/(dashboard)/club-edukai/page.tsx @@ -1,14 +1,11 @@ "use client"; -import { Users, Calendar, Trophy, Star, UserPlus, BookOpen, FileText, Search, Filter, X } from "lucide-react"; +import { Users, Calendar, Trophy, Star, UserPlus, BookOpen, Search, Filter, X } from "lucide-react"; import { PublicCourseCard } from "@/components/club/PublicCourseCard"; -import { PublicSheetCard } from "@/components/club/PublicSheetCard"; -import { useCourseService, useSummarySheetService } from "@/services"; +import { useCourseService } from "@/services"; import { useEffect, useState } from "react"; import { Loader2 } from "lucide-react"; -type TabType = "courses" | "sheets"; - interface PublicCourse { _id: string; title: string; @@ -22,23 +19,9 @@ interface PublicCourse { createdAt: string; } -interface PublicSheet { - _id: string; - title?: string; - author: { - firstName: string; - lastName: string; - username: string; - }; - createdAt: string; -} - export default function ClubEdukaiPage() { - const [activeTab, setActiveTab] = useState("courses"); const [courses, setCourses] = useState([]); - const [sheets, setSheets] = useState([]); const [loadingCourses, setLoadingCourses] = useState(true); - const [loadingSheets, setLoadingSheets] = useState(true); // Filters state const [searchQuery, setSearchQuery] = useState(""); @@ -47,7 +30,6 @@ export default function ClubEdukaiPage() { const [showFilters, setShowFilters] = useState(false); const { getPublicCourses } = useCourseService(); - const { getPublicSheets } = useSummarySheetService(); // Extract unique subjects and levels from courses const subjects = Array.from(new Set(courses.map(c => c.subject))).sort(); @@ -64,15 +46,6 @@ export default function ClubEdukaiPage() { return matchesSearch && matchesSubject && matchesLevel; }); - // Filter sheets based on search - const filteredSheets = sheets.filter(sheet => { - const matchesSearch = (sheet.title?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false) || - sheet.author.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || - sheet.author.lastName.toLowerCase().includes(searchQuery.toLowerCase()); - - return matchesSearch; - }); - // Reset filters const resetFilters = () => { setSearchQuery(""); @@ -100,25 +73,6 @@ export default function ClubEdukaiPage() { fetchPublicCourses(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - - useEffect(() => { - async function fetchPublicSheets() { - setLoadingSheets(true); - try { - const response = await getPublicSheets(); - if (response.status === "success") { - setSheets(response.items || []); - } - } catch (error) { - console.error("Error fetching public sheets:", error); - } finally { - setLoadingSheets(false); - } - } - - fetchPublicSheets(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); return (
{/* Beautiful Header Section */} @@ -240,47 +194,18 @@ export default function ClubEdukaiPage() {
- {/* Content Area with Tabs */} + {/* Content Area */}
- {/* Tabs Navigation */} -
- - +
{/* Search and Filters */} @@ -291,30 +216,28 @@ export default function ClubEdukaiPage() { setSearchQuery(e.target.value)} className="w-full pl-12 pr-4 py-3 rounded-xl border border-gray-200 bg-white/70 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" /> - {activeTab === "courses" && ( - - )} + {hasActiveFilters && ( diff --git a/app/(dashboard)/library/page.tsx b/app/(dashboard)/library/page.tsx index 2239360..5416bf1 100644 --- a/app/(dashboard)/library/page.tsx +++ b/app/(dashboard)/library/page.tsx @@ -19,6 +19,8 @@ import { useCourseService } from "@/services"; import { RotateCcw } from "lucide-react"; import { useEffect, useState } from "react"; +import type { VisibilityType } from "@/lib/types/visibility"; + // Define the extended course type for the table interface ExtendedCourseData { _id?: string; @@ -32,7 +34,7 @@ interface ExtendedCourseData { quizzes: string[]; exams: string[]; summarySheets: unknown[]; - isShared?: boolean; + visibility?: VisibilityType; } // Type for API response that might have different property names @@ -53,7 +55,7 @@ type ApiCourseData = { quizzes: string[]; exams: string[]; summarySheets: unknown[]; - isShared?: boolean; + visibility?: VisibilityType; }; export default function LibraryPage() { @@ -200,9 +202,6 @@ export default function LibraryPage() { setUserCourses(extendedCourses); } else { // If response is null or not an array, set empty array - console.warn( - "Failed to load courses or received invalid response" - ); setUserCourses([]); } }; @@ -359,7 +358,7 @@ export default function LibraryPage() { author: course.author, createdAt: course.createdAt, isPublished: course.isPublished, - isShared: course.isShared, + visibility: course.visibility, }))} isLoading={false} /> diff --git a/components/course/Header.tsx b/components/course/Header.tsx index be5a462..4f74583 100644 --- a/components/course/Header.tsx +++ b/components/course/Header.tsx @@ -3,6 +3,7 @@ import { OwnerBadge } from "@/components/badge/OwnerBadge"; import { SubjectBadge } from "@/components/badge/SubjectBadge"; import { Button } from "@/components/ui/button"; import { useSession } from "@/hooks/useSession"; +import type { VisibilityType } from "@/lib/types/visibility"; import { BicepsFlexed, CircleStop, @@ -17,7 +18,7 @@ type CourseData = { title?: string; subject?: string; level?: string; - isShared?: boolean; + visibility?: VisibilityType; isOwner?: boolean; author?: { _id?: string; diff --git a/components/library/CourseCard.tsx b/components/library/CourseCard.tsx index 5aa1edc..289e63e 100644 --- a/components/library/CourseCard.tsx +++ b/components/library/CourseCard.tsx @@ -4,6 +4,8 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { formatDate } from "@/lib/date-format"; +import type { VisibilityType } from "@/lib/types/visibility"; +import { Visibility } from "@/lib/types/visibility"; import { BookOpen, Calendar, Eye, GraduationCap, User, Globe, Lock } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -16,7 +18,7 @@ export type CourseCardProps = { author: string; createdAt: string; isPublished: boolean; - isShared?: boolean; + visibility?: VisibilityType; }; export const CourseCard = ({ @@ -27,7 +29,7 @@ export const CourseCard = ({ author, createdAt, isPublished = true, - isShared = false, + visibility = Visibility.PRIVATE, }: CourseCardProps) => { const router = useRouter(); @@ -141,12 +143,12 @@ export const CourseCard = ({ + + + + {/* Content Section */} +
+
+ + + {authorName} + +
+
+ + + {formatDate( + new Date(createdAt).toLocaleDateString( + "fr-FR" + ) + )} + +
+
+ + {/* Action Section */} +
+ + + +
+ + + + ); +}; diff --git a/lib/utils/keyboard.ts b/lib/utils/keyboard.ts new file mode 100644 index 0000000..058b47c --- /dev/null +++ b/lib/utils/keyboard.ts @@ -0,0 +1,24 @@ +/** + * Handler for keyboard events to make clickable divs accessible + * Triggers the callback when Enter or Space is pressed + */ +export function handleKeyboardClick( + event: React.KeyboardEvent, + callback: () => void +) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + callback(); + } +} + +/** + * Props to add keyboard accessibility to clickable elements + */ +export function makeKeyboardAccessible(onClick: () => void) { + return { + role: "button" as const, + tabIndex: 0, + onKeyDown: (e: React.KeyboardEvent) => handleKeyboardClick(e, onClick), + }; +} From df1fbab7707e33ca52ee4124090387cb5bd731cb Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 18:24:55 +0100 Subject: [PATCH 16/87] refactor: eliminate code duplication and modernize auth pages - Refactor PublicCourseCard and PublicSheetCard to use shared PublicContentCard base - Reduces duplication by 69.2% in PublicCourseCard and 25% in PublicSheetCard - Modernize auth pages with new AuthPageLayout and feature components - Update auth component exports for new modular structure --- app/(auth)/auth/page.tsx | 114 ++++------------- app/(auth)/change-password/page.tsx | 45 ++----- app/(auth)/reset-password/page.tsx | 36 +----- components/auth/index.ts | 5 + components/club/PublicCourseCard.tsx | 181 ++++++++++----------------- components/club/PublicSheetCard.tsx | 88 +++---------- 6 files changed, 127 insertions(+), 342 deletions(-) diff --git a/app/(auth)/auth/page.tsx b/app/(auth)/auth/page.tsx index 29a7527..1ac17bc 100644 --- a/app/(auth)/auth/page.tsx +++ b/app/(auth)/auth/page.tsx @@ -3,7 +3,11 @@ import Image from "next/image"; import { useState, useEffect } from "react"; import { Card, CardContent } from "@/components/ui/card"; -import { AuthContainer } from "@/components/auth"; +import { + AuthContainer, + EdukaiHeader, + FeatureCard, +} from "@/components/auth"; export default function Authpage() { const [currentSlide, setCurrentSlide] = useState(0); @@ -37,25 +41,7 @@ export default function Authpage() {
- {/* Header */} -
-
-
- Logo Edukai -
-

- Edukai -

-
-
- - {/* Auth Container */} +
-
-
-
-
-
-
-

- IA Avancée -

-

- Questions - personnalisées par - l'IA -

-
-
-
- -
-
-
-
-
-
-

- Rapide -

-

- Résultats en moins - de 20s -

-
-
-
- -
-
-
-
-
-
-

- Adaptatif -

-

- S'adapte à ton - niveau d'étude -

-
-
-
- -
-
-
-
-
-
-

- Intelligent -

-

- Analyse tes points - faibles -

-
-
-
+ + + +
diff --git a/app/(auth)/change-password/page.tsx b/app/(auth)/change-password/page.tsx index 4bf8452..aa04f19 100644 --- a/app/(auth)/change-password/page.tsx +++ b/app/(auth)/change-password/page.tsx @@ -1,9 +1,7 @@ "use client"; -import Image from "next/image"; import { useRouter } from "next/navigation"; -import { Card, CardContent } from "@/components/ui/card"; -import { ChangePasswordForm } from "@/components/auth"; +import { AuthPageLayout, ChangePasswordForm } from "@/components/auth"; export default function ChangePasswordPage() { const router = useRouter(); @@ -24,39 +22,12 @@ export default function ChangePasswordPage() { }; return ( -
-
-
- - - {/* Header */} -
-
-
- Logo Edukai -
-

- Edukai -

-
-
- - {/* Change Password Form */} - -
-
-
-
-
+ + + ); } diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx index 322ed68..591aacd 100644 --- a/app/(auth)/reset-password/page.tsx +++ b/app/(auth)/reset-password/page.tsx @@ -1,40 +1,14 @@ "use client"; -import Image from "next/image"; -import { Suspense } from "react"; -import { Card, CardContent } from "@/components/ui/card"; +import { AuthPageLayout } from "@/components/auth"; import ResetPasswordForm from "@/components/auth/reset-password-with-token-form"; +import { Suspense } from "react"; function ResetPasswordContent() { return ( -
-
- - - {/* Header */} -
-
-
- Logo Edukai -
-

- Edukai -

-
-
- - {/* Reset Password Form */} - -
-
-
-
+ + + ); } diff --git a/components/auth/index.ts b/components/auth/index.ts index 9ec8be0..838bc69 100644 --- a/components/auth/index.ts +++ b/components/auth/index.ts @@ -7,6 +7,11 @@ export { ChangePasswordForm } from "./change-password-form"; // Auth Container export { AuthContainer } from "./auth-container"; +// Auth Layout Components +export { EdukaiHeader } from "./edukai-header"; +export { AuthPageLayout } from "./auth-page-layout"; +export { FeatureCard } from "./feature-card"; + // Types export type { SigninFormProps } from "./signin-form"; export type { SignupFormProps } from "./signup-form"; diff --git a/components/club/PublicCourseCard.tsx b/components/club/PublicCourseCard.tsx index 7e166d3..c954964 100644 --- a/components/club/PublicCourseCard.tsx +++ b/components/club/PublicCourseCard.tsx @@ -1,11 +1,8 @@ "use client"; -import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { formatDate } from "@/lib/date-format"; -import { BookOpen, Calendar, User, Eye, GraduationCap } from "lucide-react"; -import Link from "next/link"; +import { BookOpen, GraduationCap } from "lucide-react"; +import { PublicContentCard } from "./PublicContentCard"; export type PublicCourseCardProps = { id: string; @@ -20,6 +17,41 @@ export type PublicCourseCardProps = { createdAt: string; }; +const getSubjectColor = (subject: string) => { + const colors = { + Mathématiques: "bg-blue-100 text-blue-800 border-blue-200", + Physique: "bg-purple-100 text-purple-800 border-purple-200", + Chimie: "bg-green-100 text-green-800 border-green-200", + Biologie: "bg-emerald-100 text-emerald-800 border-emerald-200", + Histoire: "bg-orange-100 text-orange-800 border-orange-200", + Géographie: "bg-yellow-100 text-yellow-800 border-yellow-200", + Français: "bg-red-100 text-red-800 border-red-200", + Anglais: "bg-indigo-100 text-indigo-800 border-indigo-200", + Espagnol: "bg-pink-100 text-pink-800 border-pink-200", + Philosophie: "bg-gray-100 text-gray-800 border-gray-200", + }; + return ( + colors[subject as keyof typeof colors] || + "bg-gray-100 text-gray-800 border-gray-200" + ); +}; + +const getLevelColor = (level: string) => { + const colors = { + Sixième: "bg-emerald-50 text-emerald-700", + Cinquième: "bg-teal-50 text-teal-700", + Quatrième: "bg-cyan-50 text-cyan-700", + Troisième: "bg-blue-50 text-blue-700", + Seconde: "bg-indigo-50 text-indigo-700", + Première: "bg-purple-50 text-purple-700", + Terminale: "bg-pink-50 text-pink-700", + TERMIIIIINALE: "bg-pink-50 text-pink-700", + }; + return ( + colors[level as keyof typeof colors] || "bg-gray-50 text-gray-700" + ); +}; + export const PublicCourseCard = ({ id, title, @@ -28,118 +60,35 @@ export const PublicCourseCard = ({ author, createdAt, }: PublicCourseCardProps) => { - const getSubjectColor = (subject: string) => { - const colors = { - Mathématiques: "bg-blue-100 text-blue-800 border-blue-200", - Physique: "bg-purple-100 text-purple-800 border-purple-200", - Chimie: "bg-green-100 text-green-800 border-green-200", - Biologie: "bg-emerald-100 text-emerald-800 border-emerald-200", - Histoire: "bg-orange-100 text-orange-800 border-orange-200", - Géographie: "bg-yellow-100 text-yellow-800 border-yellow-200", - Français: "bg-red-100 text-red-800 border-red-200", - Anglais: "bg-indigo-100 text-indigo-800 border-indigo-200", - Espagnol: "bg-pink-100 text-pink-800 border-pink-200", - Philosophie: "bg-gray-100 text-gray-800 border-gray-200", - }; - return ( - colors[subject as keyof typeof colors] || - "bg-gray-100 text-gray-800 border-gray-200" - ); - }; - - const getLevelColor = (level: string) => { - const colors = { - Sixième: "bg-emerald-50 text-emerald-700", - Cinquième: "bg-teal-50 text-teal-700", - Quatrième: "bg-cyan-50 text-cyan-700", - Troisième: "bg-blue-50 text-blue-700", - Seconde: "bg-indigo-50 text-indigo-700", - Première: "bg-purple-50 text-purple-700", - Terminale: "bg-pink-50 text-pink-700", - TERMIIIIINALE: "bg-pink-50 text-pink-700", - }; - return ( - colors[level as keyof typeof colors] || "bg-gray-50 text-gray-700" - ); - }; - - const authorName = `${author.firstName} ${author.lastName}`; + const badges = ( + <> + + {subject} + +
+ + + {level} + +
+ + ); return ( - - -
- {/* Header Section */} -
-
-
-
- -
- - {subject} - -
-

- {title} -

-
- - - -
- - {/* Content Section */} -
-
- - - {level} - -
-
- - - {authorName} - -
-
- - - {formatDate( - new Date(createdAt).toLocaleDateString( - "fr-FR" - ) - )} - -
-
- - {/* Action Section */} -
- - - -
-
-
-
+ ); }; diff --git a/components/club/PublicSheetCard.tsx b/components/club/PublicSheetCard.tsx index 8ec6496..244342e 100644 --- a/components/club/PublicSheetCard.tsx +++ b/components/club/PublicSheetCard.tsx @@ -1,10 +1,7 @@ "use client"; -import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { formatDate } from "@/lib/date-format"; -import { FileText, Calendar, User, Eye } from "lucide-react"; -import Link from "next/link"; +import { FileText } from "lucide-react"; +import { PublicContentCard } from "./PublicContentCard"; export type PublicSheetCardProps = { id: string; @@ -23,73 +20,24 @@ export const PublicSheetCard = ({ author, createdAt, }: PublicSheetCardProps) => { - const authorName = `${author.firstName} ${author.lastName}`; const sheetTitle = title || `Fiche de révision`; - return ( - - -
- {/* Header Section */} -
-
-
-
- -
- - Fiche de révision - -
-

- {sheetTitle} -

-
- - - -
- - {/* Content Section */} -
-
- - - {authorName} - -
-
- - - {formatDate( - new Date(createdAt).toLocaleDateString( - "fr-FR" - ) - )} - -
-
+ const badges = ( + + Fiche de révision + + ); - {/* Action Section */} -
- - - -
-
-
-
+ return ( + ); }; From 8cae374307dad26a3525a1652dbaeacf34683a26 Mon Sep 17 00:00:00 2001 From: TristanHourtoulle Date: Tue, 4 Nov 2025 18:25:41 +0100 Subject: [PATCH 17/87] fix: resolve SonarCloud issues and remove debug logs SonarCloud fixes: - Fix locale-aware string sorting for proper French character handling - Resolve function name conflict with built-in Error class - Remove unnecessary await on synchronous cookies() call - Add keyboard accessibility with role, tabIndex, and onKeyDown handlers - Associate form labels with controls using htmlFor and id attributes - Replace array index keys with stable unique keys - Optimize UserContext with useMemo to prevent unnecessary re-renders - Extract nested ternaries into clear helper functions for readability - Replace role="button" divs with semantic HTML button and label elements - Fix drag event handler types for better TypeScript compliance - Add console.debug for appropriate debugging in error handlers - Remove unused imports Code cleanup: - Remove temporary console.log debug statements from components - Remove debug logging from middleware authentication flow - Clean up debug logs from auth container success handler --- app/(dashboard)/admin/tickets/page.tsx | 27 +++-- app/(dashboard)/club-edukai/page.tsx | 103 +++++++++++------- app/(dashboard)/layout.tsx | 4 +- .../library/[id]/settings/page.tsx | 2 +- app/(dashboard)/tickets/[id]/page.tsx | 4 + app/(dashboard)/tickets/page.tsx | 2 +- app/error.tsx | 2 +- components/auth/auth-container.tsx | 1 - components/course/sections/exams/ExamCard.tsx | 58 ++++++---- .../summary-sheets/AddSummarySheet.tsx | 14 +-- components/data-table/ProjectActions.tsx | 1 - components/data-table/Tabs/TabItem.tsx | 5 +- components/generator/file-upload.tsx | 15 ++- components/input/file-input.tsx | 14 +-- .../settings/profile-picture-upload.tsx | 17 +-- components/sidebar/beta-badge.tsx | 1 - components/ticket/admin-ticket-card.tsx | 11 +- contexts/UserContext.tsx | 19 ++-- hooks/useFileDecompression.ts | 1 + hooks/useSheet.ts | 1 - middleware.ts | 2 - 21 files changed, 170 insertions(+), 134 deletions(-) diff --git a/app/(dashboard)/admin/tickets/page.tsx b/app/(dashboard)/admin/tickets/page.tsx index e4f00ac..9fa2612 100644 --- a/app/(dashboard)/admin/tickets/page.tsx +++ b/app/(dashboard)/admin/tickets/page.tsx @@ -190,7 +190,6 @@ export default function AdminTicketsPage() { // Handle ticket assignment (placeholder) const handleAssign = async (ticketId: string) => { - console.log("Assign ticket:", ticketId); // TODO: Implement assignment dialog }; @@ -222,6 +221,18 @@ export default function AdminTicketsPage() { } }; + // Get empty state message based on active filters + const getEmptyStateMessage = () => { + const hasFilters = searchTerm || statusFilter !== "all" || priorityFilter !== "all"; + if (hasFilters) { + return "Aucun ticket ne correspond à vos critères de recherche."; + } + return "Aucun ticket n'a été trouvé dans le système."; + }; + + // Check if filters are active + const hasActiveFilters = searchTerm || statusFilter !== "all" || priorityFilter !== "all"; + // Don't render anything if still loading if (session.loading) { return ( @@ -385,9 +396,9 @@ export default function AdminTicketsPage() {
{isLoading ? ( // Loading skeleton - [...Array(LOADING_SKELETON_COUNT)].map((_, i) => ( + Array.from({ length: LOADING_SKELETON_COUNT }, (_, i) => ( @@ -406,15 +417,9 @@ export default function AdminTicketsPage() { Aucun ticket trouvé

- {searchTerm || - statusFilter !== "all" || - priorityFilter !== "all" - ? "Aucun ticket ne correspond à vos critères de recherche." - : "Aucun ticket n'a été trouvé dans le système."} + {getEmptyStateMessage()}

- {(searchTerm || - statusFilter !== "all" || - priorityFilter !== "all") && ( + {hasActiveFilters && (