diff --git a/app/components/combobox.tsx b/app/components/combobox.tsx index 1985b4e..d8586bf 100644 --- a/app/components/combobox.tsx +++ b/app/components/combobox.tsx @@ -22,18 +22,16 @@ export function Combobox({ defaultText = "Select value...", placeholderText = "Search...", emptyText = "No value found.", - renderValue = (value) => value - ? values.find((current) => current.value === value)?.label! - : defaultText, + current, setValue }: { values?: { value: string; label: string }[] emptyText?: string placeholderText?: string defaultText?: string - renderValue?: (value: string) => string + current?: string + setValue: (value: string) => void }) { const [open, setOpen] = React.useState(false) - const [current, setValue] = React.useState("") return ( @@ -44,7 +42,9 @@ export function Combobox({ aria-expanded={open} className="w-full justify-between" > - {renderValue(current)} + {current + ? values.find((value) => value.value === current)?.label! + : defaultText} @@ -59,7 +59,7 @@ export function Combobox({ key={value} value={value} onSelect={(currentValue) => { - setValue(currentValue === value ? "" : currentValue) + setValue(currentValue === current ? "" : currentValue) setOpen(false) }} > diff --git a/app/components/input-otp.tsx b/app/components/input-otp.tsx index 1d12352..cd305b9 100644 --- a/app/components/input-otp.tsx +++ b/app/components/input-otp.tsx @@ -22,20 +22,27 @@ export default function OtpInput({ setIsVerifying(true); setOtpError(null); verifyOtp({ otpCode, email }) - .then(() => { + .then((data) => { setIsVerifying(false); - // TODO: Check if new user ?? - navigate("/success?email=" + encodeURIComponent(email), { - replace: true, - }); + // Check if user is registered + if (data.token) { + navigate(`/register?email=${data.email}&token=${data.token}`, { + replace: true, + }); + } else { + navigate("/", { + replace: true, + }) + } }) - .catch((error) => + .catch((error) => { setOtpError( error instanceof Error ? error.message : "Code de vérification invalide" ) - ); + setIsVerifying(false) + }); }; return ( diff --git a/app/lib/api.ts b/app/lib/api.ts index ec12106..f2f1a3c 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -1,6 +1,6 @@ const BASE_URL = import.meta.env.VITE_API_URL; -const makeRequest = async ( +const makePostRequest = async ( url: string, body: object, error = "Une erreur est survenue", @@ -10,8 +10,10 @@ const makeRequest = async ( method, headers: { "Content-Type": "application/json", + "Accept": "application/json", }, body: JSON.stringify(body), + credentials: "include", // Include cookies for authentication }); const data = await response.json(); @@ -19,8 +21,24 @@ const makeRequest = async ( return data?.data || data; }; +// TODO: Use swr or react-query for caching and revalidation +// TODO: Cache all to localStorage or IndexedDB for offline support +const makeRequest = async ( + url: string, + error = "Une erreur est survenue", +) => { + const response = await fetch(BASE_URL + url, { credentials: "include"}); + + const data = await response.json(); + if (!response.ok) throw new Error(data.error || error); + return data?.data || data; +}; + +/** + * === AUTH API === + */ export const requestLogin = async (email: string, token: string) => { - return makeRequest( + return makePostRequest( "/auth/request", { email, token }, "Échec de la demande de connexion" @@ -34,17 +52,39 @@ export const verifyOtp = async ({ otpCode: string; email: string; }) => { - return makeRequest( + return makePostRequest( "/auth/verify", { email, code: otpCode }, "Code de vérification invalide" ); }; -export const sendOtp = async (email: string) => { - return makeRequest( - "/auth/send-otp", - { email }, - "Échec de l'envoi du code de vérification" +export const getClasses = async () => { + try { + const res = await fetch("/classes.json"); + return res.json(); + } catch (error) { + console.error("Error fetching classes:", error); + return []; + } +}; + +export const registerUser = async ( + firstName: string, + lastName: string, + className: string, + token: string +) => { + return makePostRequest( + "/auth/register", + { firstName, lastName, className, token }, + "Échec de l'inscription" ); }; + +/** + * === COLLES API === + */ +export const getColles = async (weekNumber: number, year: number) => { + return makeRequest(`/colles?week=${weekNumber}&year=${year}`, "Échec de la récupération des colles"); +}; diff --git a/app/routes.ts b/app/routes.ts index 16bb858..cc6340d 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,7 +1,8 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ - index("routes/login.tsx"), + index("routes/index.tsx"), + route("/login", "routes/login.tsx"), route("/verify", "routes/verify.tsx"), route("/register", "routes/register.tsx"), ] satisfies RouteConfig; diff --git a/app/routes/index.tsx b/app/routes/index.tsx new file mode 100644 index 0000000..eb38dd6 --- /dev/null +++ b/app/routes/index.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import { getColles } from "~/lib/api"; + +export default function Index() { + const [colles, setColles] = useState([]); + + useEffect(() => { + // Fetch colles data from the API + getColles(12, 2025).then((data) => { + setColles(data); + }).catch((error) => { + console.error("Error fetching colles:", error); + }); + }, []); + + return ( +
+
+

Welcome to Khollisé!

+

Got {colles.length} colles this week.

+
+
+ ); +} diff --git a/app/routes/register.tsx b/app/routes/register.tsx index 4ab9e89..e83155b 100644 --- a/app/routes/register.tsx +++ b/app/routes/register.tsx @@ -4,11 +4,12 @@ import { AlertCircleIcon, IdCardIcon, LoaderCircle, LogIn, MailIcon } from "luci import { Label } from "~/components/ui/label"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import AuthLayout from "~/layout"; import { useNavigate, useSearchParams } from "react-router"; import { capitalizeFirstLetter } from "~/lib/utils"; import { Combobox } from "~/components/combobox"; +import { getClasses, registerUser } from "~/lib/api"; export function meta({ }: Route.MetaArgs) { return [ @@ -25,6 +26,22 @@ export default function Register() { const [emailFirst, emailLast] = getNameFromEmail(email); const [firstName, setFirstName] = useState(emailFirst); const [lastName, setLastName] = useState(emailLast); + const [className, setClassName] = useState(""); + + const token = searchParams.get("token"); + useEffect(() => { + if (!email || !token) { + navigate("/login", { replace: true }); + return; + } + }, [email, token]); + + const [classes, setClasses] = useState<{ value: string; label: string }[]>([]); + useEffect(() => { + getClasses().then((data) => { + setClasses(data); + }) + }, []); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -32,16 +49,18 @@ export default function Register() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); - // TODO: Validate... - // TODO: Check if combobox is valid (not empty !!) + + // Validate inputs + if (!firstName || !lastName) return setError("Veuillez compléter votre prénom et nom."); + if (!className) return setError("Veuillez sélectionner votre classe."); setIsLoading(true); - // TODO: Fetch function - const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - await sleep(5000) - .then((data) => { + + await registerUser(firstName, lastName, className, token!) + .then(() => { setIsLoading(false); - // TODO: Callback + navigate("/", { + replace: true, + }) }) .catch((err) => { setIsLoading(false); @@ -98,19 +117,13 @@ export default function Register() {
- {/* TODO: Pass ref to combobox to validate */}
diff --git a/app/routes/success.tsx b/app/routes/unused/success.tsx similarity index 98% rename from app/routes/success.tsx rename to app/routes/unused/success.tsx index 9d14578..1c134ab 100644 --- a/app/routes/success.tsx +++ b/app/routes/unused/success.tsx @@ -1,4 +1,4 @@ -import type { Route } from "./+types/success"; +// import type { Route } from "./+types/success"; import { useState, useEffect } from "react"; import { CheckCircleIcon, @@ -15,7 +15,7 @@ import { } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; -export function meta({}: Route.MetaArgs) { +export function meta() { return [ { title: "Khollisé - Connexion" }, { name: "description", content: "Connexion réussie à Khollisé" }, diff --git a/app/routes/verify.tsx b/app/routes/verify.tsx index 72e7c53..b16702b 100644 --- a/app/routes/verify.tsx +++ b/app/routes/verify.tsx @@ -13,7 +13,7 @@ import { Alert, AlertDescription } from "~/components/ui/alert"; import { Button } from "~/components/ui/button"; import AuthLayout from "~/layout"; -export function meta({}: Route.MetaArgs) { +export function meta({ }: Route.MetaArgs) { return [ { title: "Khollisé - Connexion" }, { name: "description", content: "Connectez-vous à Khollisé" }, @@ -24,10 +24,13 @@ export default function Verify() { const navigate = useNavigate(); const [searchParams] = useSearchParams() const email = searchParams.get("email"); - if (!email) { - // TODO: Redirect to login page - return - } + + useEffect(() => { + if (!email) { + navigate("/login", { replace: true }); + return; + } + }, [email]); // const [isResending, setIsResending] = useState(false); // const [resendSuccess, setResendSuccess] = useState(false); @@ -111,12 +114,12 @@ export default function Verify() { Entrez le code de vérification -{/* TODO: Resend OTP + {/* TODO: Resend OTP {resendSuccess && ( diff --git a/public/classes.json b/public/classes.json new file mode 100644 index 0000000..c3a2d0f --- /dev/null +++ b/public/classes.json @@ -0,0 +1,6 @@ +[ + { + "value": "MPSI 2", + "label": "MPSI 2 - Taupe" + } +] \ No newline at end of file