feat: rework registration form
This commit is contained in:
parent
c7d7fd53cb
commit
3549281b73
4 changed files with 103 additions and 57 deletions
|
|
@ -22,7 +22,9 @@ export function Combobox({
|
||||||
defaultText = "Select value...",
|
defaultText = "Select value...",
|
||||||
placeholderText = "Search...",
|
placeholderText = "Search...",
|
||||||
emptyText = "No value found.",
|
emptyText = "No value found.",
|
||||||
current, setValue
|
current,
|
||||||
|
setValue,
|
||||||
|
disabled = false,
|
||||||
}: {
|
}: {
|
||||||
values?: { value: string; label: string }[]
|
values?: { value: string; label: string }[]
|
||||||
emptyText?: string
|
emptyText?: string
|
||||||
|
|
@ -30,17 +32,19 @@ export function Combobox({
|
||||||
defaultText?: string
|
defaultText?: string
|
||||||
current?: string
|
current?: string
|
||||||
setValue: (value: string) => void
|
setValue: (value: string) => void
|
||||||
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open && !disabled} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-full justify-between"
|
className="w-full justify-between"
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{current
|
{current
|
||||||
? values.find((value) => value.value === current)?.label!
|
? values.find((value) => value.value === current)?.label!
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./components/ui/button";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
const SUPPORT_MAIL = "mailto:" + import.meta.env.VITE_SUPPORT_EMAIL;
|
export const SUPPORT_MAIL = "mailto:" + import.meta.env.VITE_SUPPORT_EMAIL;
|
||||||
|
|
||||||
export function AuthLayout({
|
export function AuthLayout({
|
||||||
children,
|
children,
|
||||||
|
|
|
||||||
|
|
@ -70,18 +70,24 @@ export const getClasses = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const registerUser = async (
|
export const registerUser = async (
|
||||||
firstName: string,
|
name: string,
|
||||||
lastName: string,
|
|
||||||
className: string,
|
className: string,
|
||||||
token: string
|
token: string
|
||||||
) => {
|
) => {
|
||||||
return makePostRequest(
|
return makePostRequest(
|
||||||
"/auth/register",
|
"/auth/register",
|
||||||
{ firstName, lastName, className, token },
|
{ name, className, token },
|
||||||
"Échec de l'inscription"
|
"Échec de l'inscription"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getNames = async (className: string) => {
|
||||||
|
return makeRequest(
|
||||||
|
`/auth/autocomplete?className=${encodeURIComponent(className)}`,
|
||||||
|
"Échec de la récupération des noms"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* === USER API ===
|
* === USER API ===
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
import { Alert, AlertDescription } from "~/components/ui/alert";
|
import { Alert, AlertDescription } from "~/components/ui/alert";
|
||||||
import { AlertCircleIcon, IdCardIcon, LoaderCircle, LogIn, MailIcon } from "lucide-react";
|
import {
|
||||||
|
AlertCircleIcon,
|
||||||
|
ArrowRight,
|
||||||
|
LoaderCircle,
|
||||||
|
LogIn,
|
||||||
|
MailIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AuthLayout } from "~/layout";
|
import { AuthLayout, SUPPORT_MAIL } from "~/layout";
|
||||||
import { useNavigate, useSearchParams } from "react-router";
|
import { useNavigate, useSearchParams } from "react-router";
|
||||||
import { capitalizeFirstLetter } from "~/lib/utils";
|
import { capitalizeFirstLetter } from "~/lib/utils";
|
||||||
import { Combobox } from "~/components/combobox";
|
import { Combobox } from "~/components/combobox";
|
||||||
import { getClasses, registerUser } from "~/lib/api";
|
import { getClasses, getNames, registerUser } from "~/lib/api";
|
||||||
|
|
||||||
export function meta() {
|
export function meta() {
|
||||||
return [
|
return [
|
||||||
|
|
@ -23,9 +29,8 @@ export default function Register() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const email = searchParams.get("email")!;
|
const email = searchParams.get("email")!;
|
||||||
const [emailFirst, emailLast] = getNameFromEmail(email);
|
const [emailFirst, emailLast] = getNameFromEmail(email);
|
||||||
const [firstName, setFirstName] = useState(emailFirst);
|
|
||||||
const [lastName, setLastName] = useState(emailLast);
|
|
||||||
const [className, setClassName] = useState("");
|
const [className, setClassName] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -35,40 +40,69 @@ export default function Register() {
|
||||||
}
|
}
|
||||||
}, [email, token]);
|
}, [email, token]);
|
||||||
|
|
||||||
const [classes, setClasses] = useState<{ value: string; label: string }[]>([]);
|
const [classes, setClasses] = useState<{ value: string; label: string }[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getClasses().then((data) => {
|
getClasses().then((data) => {
|
||||||
setClasses(data);
|
setClasses(data);
|
||||||
})
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [names, setNames] = useState<{ value: string; label: string }[]>([]);
|
||||||
|
const hasSubmitted = names.length > 0;
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Validate inputs
|
if (!hasSubmitted) {
|
||||||
if (!firstName || !lastName) return setError("Veuillez compléter votre prénom et nom.");
|
// Validate inputs
|
||||||
if (!className) return setError("Veuillez sélectionner votre classe.");
|
if (!className) return setError("Veuillez sélectionner votre classe.");
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
await registerUser(firstName, lastName, className, token!)
|
await getNames(className)
|
||||||
.then(() => {
|
.then((names) => {
|
||||||
setIsLoading(false);
|
setNames(names);
|
||||||
navigate("/", {
|
setIsLoading(false);
|
||||||
replace: true,
|
|
||||||
|
// Try to default name from email
|
||||||
|
const possibleName = names.find(
|
||||||
|
(n: { label: string }) => n.label === `${emailFirst} ${emailLast}`
|
||||||
|
);
|
||||||
|
if (possibleName) {
|
||||||
|
setName(possibleName.value);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
.catch((err) => {
|
setIsLoading(false);
|
||||||
setIsLoading(false);
|
setError(err.message);
|
||||||
setError(err.message);
|
});
|
||||||
});
|
} else {
|
||||||
|
// Register user
|
||||||
|
if (!name) return setError("Veuillez compléter votre nom.");
|
||||||
|
await registerUser(name, className, token!)
|
||||||
|
.then(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate("/", {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout title="Inscription" description="Créez votre compte pour accéder à Khollisé.">
|
<AuthLayout
|
||||||
|
title="Inscription"
|
||||||
|
description="Créez votre compte pour accéder à Khollisé."
|
||||||
|
>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="email">Email</Label>
|
||||||
|
|
@ -87,32 +121,6 @@ export default function Register() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 flex gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="password">Prénom</Label>
|
|
||||||
<Input
|
|
||||||
id="firstName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Claire"
|
|
||||||
value={firstName}
|
|
||||||
onChange={(e) => setFirstName(capitalizeFirstLetter(e.target.value))}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Nom</Label>
|
|
||||||
<Input
|
|
||||||
id="lastName"
|
|
||||||
type="text"
|
|
||||||
placeholder="DUPONT"
|
|
||||||
value={lastName}
|
|
||||||
onChange={(e) => setLastName(e.target.value.toUpperCase())}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">Classe</Label>
|
<Label htmlFor="password">Classe</Label>
|
||||||
<div className="block">
|
<div className="block">
|
||||||
|
|
@ -123,10 +131,34 @@ export default function Register() {
|
||||||
placeholderText="Rechercher"
|
placeholderText="Rechercher"
|
||||||
current={className}
|
current={className}
|
||||||
setValue={setClassName}
|
setValue={setClassName}
|
||||||
|
disabled={names.length > 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Si votre classe n'est pas dans la liste, contactez-nous par{" "}
|
||||||
|
<a href={SUPPORT_MAIL} className="text-blue-500 hover:underline">
|
||||||
|
mail
|
||||||
|
</a>{" "}
|
||||||
|
pour l'ajouter.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{hasSubmitted && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="names">Prénom et Nom</Label>
|
||||||
|
<div className="block">
|
||||||
|
<Combobox
|
||||||
|
defaultText="Sélectionnez votre nom..."
|
||||||
|
values={names}
|
||||||
|
emptyText="Aucun nom trouvé"
|
||||||
|
placeholderText="Rechercher"
|
||||||
|
current={name}
|
||||||
|
setValue={setName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert variant="destructive" className="pb-2">
|
<Alert variant="destructive" className="pb-2">
|
||||||
<AlertCircleIcon className="h-4 w-4" />
|
<AlertCircleIcon className="h-4 w-4" />
|
||||||
|
|
@ -138,12 +170,16 @@ export default function Register() {
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<LoaderCircle className="h-4 w-4 animate-spin mr-2" />
|
<LoaderCircle className="h-4 w-4 animate-spin mr-2" />
|
||||||
Inscription en cours...
|
Validation en cours...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LogIn className="h-4 w-4 mr-2" />
|
{hasSubmitted ? (
|
||||||
S'inscrire
|
<LogIn className="h-4 w-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<ArrowRight className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{hasSubmitted ? "S'inscrire" : "Continuer"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue