feat: introduce register API
This commit is contained in:
parent
83579f5c60
commit
77ccdc6862
9 changed files with 145 additions and 51 deletions
|
|
@ -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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
|
|
@ -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}
|
||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -59,7 +59,7 @@ export function Combobox({
|
|||
key={value}
|
||||
value={value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
setValue(currentValue === current ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
24
app/routes/index.tsx
Normal file
24
app/routes/index.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h1 className="text-2xl font-bold">Welcome to Khollisé!</h1>
|
||||
<p>Got {colles.length} colles this week.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<string | null>(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() {
|
|||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Classe</Label>
|
||||
<div className="block">
|
||||
{/* TODO: Pass ref to combobox to validate */}
|
||||
<Combobox
|
||||
defaultText="Sélectionnez votre classe..."
|
||||
// TODO: Fetch function
|
||||
values={[
|
||||
{ value: "1", label: "1ère" },
|
||||
{ value: "2", label: "2ème" },
|
||||
{ value: "3", label: "3ème" },
|
||||
{ value: "4", label: "4ème" },
|
||||
{ value: "5", label: "5ème" },
|
||||
]}
|
||||
emptyText="Chargement..."
|
||||
values={classes}
|
||||
emptyText="Aucune classe trouvée"
|
||||
placeholderText="Rechercher"
|
||||
current={className}
|
||||
setValue={setClassName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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é" },
|
||||
|
|
@ -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
|
||||
</h3>
|
||||
<OtpInput
|
||||
email={email}
|
||||
email={email!}
|
||||
setOtpError={setOtpError}
|
||||
otpError={otpError}
|
||||
/>
|
||||
|
||||
{/* TODO: Resend OTP
|
||||
{/* TODO: Resend OTP
|
||||
{resendSuccess && (
|
||||
<Alert className="bg-green-50 border-green-200 mt-2">
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-600" />
|
||||
|
|
|
|||
6
public/classes.json
Normal file
6
public/classes.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"value": "MPSI 2",
|
||||
"label": "MPSI 2 - Taupe"
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue