Compare commits
6 commits
85e2552db8
...
0684a0649b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0684a0649b | ||
|
|
3549281b73 | ||
|
|
c7d7fd53cb | ||
|
|
ea585bd748 | ||
|
|
9ea54ed90f | ||
|
|
ef2d65d261 |
12 changed files with 293 additions and 153 deletions
|
|
@ -22,7 +22,9 @@ export function Combobox({
|
|||
defaultText = "Select value...",
|
||||
placeholderText = "Search...",
|
||||
emptyText = "No value found.",
|
||||
current, setValue
|
||||
current,
|
||||
setValue,
|
||||
disabled = false,
|
||||
}: {
|
||||
values?: { value: string; label: string }[]
|
||||
emptyText?: string
|
||||
|
|
@ -30,17 +32,19 @@ export function Combobox({
|
|||
defaultText?: string
|
||||
current?: string
|
||||
setValue: (value: string) => void
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open && !disabled} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
disabled={disabled}
|
||||
>
|
||||
{current
|
||||
? values.find((value) => value.value === current)?.label!
|
||||
|
|
|
|||
|
|
@ -79,6 +79,17 @@ export default function Home() {
|
|||
isLoading,
|
||||
});
|
||||
|
||||
// Error handling (after all hooks)
|
||||
if (error)
|
||||
return (
|
||||
<Error
|
||||
title="Impossible de charger les colles"
|
||||
message={error?.toString()}
|
||||
code={500}
|
||||
description="Une erreur s'est produite lors du chargement de la liste des colles."
|
||||
/>
|
||||
);
|
||||
|
||||
// TODO: FAVORITES
|
||||
const useToggleStar = (auth: any) => {};
|
||||
// TODO: FILTERS
|
||||
|
|
@ -127,17 +138,6 @@ export default function Home() {
|
|||
// setTimeout(restoreScrollPosition, 500);
|
||||
// }, [location]);
|
||||
|
||||
// Error handling (after all hooks)
|
||||
if (error)
|
||||
return (
|
||||
<Error
|
||||
title="Impossible de charger les colles"
|
||||
message={error?.toString()}
|
||||
code={500}
|
||||
description="Une erreur s'est produite lors du chargement de la liste des colles."
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-20 md:pb-0">
|
||||
{/* Week Navigation */}
|
||||
|
|
|
|||
20
app/components/loader.tsx
Normal file
20
app/components/loader.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export default function Loader() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-3 h-3 bg-primary rounded-full animate-bounce"></div>
|
||||
<div
|
||||
className="w-3 h-3 bg-primary rounded-full animate-bounce"
|
||||
style={{ animationDelay: "0.1s" }}
|
||||
></div>
|
||||
<div
|
||||
className="w-3 h-3 bg-primary rounded-full animate-bounce"
|
||||
style={{ animationDelay: "0.2s" }}
|
||||
></div>
|
||||
</div>
|
||||
<h2 className="text-primary text-sm">Chargement...</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
app/components/theme-provider.tsx
Normal file
100
app/components/theme-provider.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
const isLocalStorageAvailable = () => {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.localStorage !== "undefined" &&
|
||||
window.localStorage !== null
|
||||
);
|
||||
};
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
if (isLocalStorageAvailable()) {
|
||||
const storedTheme = localStorage.getItem(storageKey) as Theme;
|
||||
return storedTheme || defaultTheme;
|
||||
}
|
||||
return defaultTheme;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useDisplayedTheme = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
if (theme === "system") {
|
||||
return {
|
||||
theme: window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light",
|
||||
setTheme,
|
||||
};
|
||||
}
|
||||
|
||||
return { theme, setTheme };
|
||||
};
|
||||
|
|
@ -17,25 +17,32 @@ import {
|
|||
Settings,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
// TODO: THEME
|
||||
// import { useTheme } from "~/components/ui/theme-provider";
|
||||
import { useDisplayedTheme } from "~/components/theme-provider";
|
||||
import { useNavigate } from "react-router";
|
||||
import { logout, type User } from "~/lib/api";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export default function UserDropdown({ user }: { user: User }) {
|
||||
// TODO: const { theme, setTheme } = useTheme();
|
||||
const [theme, setTheme] = useState("light");
|
||||
const { theme, setTheme } = useDisplayedTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
navigate("/login", { replace: true });
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
}
|
||||
const useLogout = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Logout, invalidate user cache, and redirect to login
|
||||
return async () => {
|
||||
try {
|
||||
await logout();
|
||||
queryClient.removeQueries({ queryKey: ["user"] });
|
||||
navigate("/login", { replace: true });
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleLogout = useLogout();
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
|
|
@ -57,9 +64,7 @@ export default function UserDropdown({ user }: { user: User }) {
|
|||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{user.fullName}
|
||||
</p>
|
||||
<p className="text-sm font-medium leading-none">{user.fullName}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{user.className}
|
||||
</p>
|
||||
|
|
@ -87,29 +92,26 @@ export default function UserDropdown({ user }: { user: User }) {
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigate("/settings");
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>
|
||||
Paramètres
|
||||
</span>
|
||||
<span>Paramètres</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigate("/progress");
|
||||
}}>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigate("/progress");
|
||||
}}
|
||||
>
|
||||
<LineChartIcon className="mr-2 h-4 w-4" />
|
||||
<span
|
||||
|
||||
>
|
||||
Progression
|
||||
</span>
|
||||
<span>Progression</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span >Se déconnecter</span>
|
||||
<span>Se déconnecter</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { Button } from "./components/ui/button";
|
||||
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({
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -70,18 +70,23 @@ export const getClasses = async () => {
|
|||
};
|
||||
|
||||
export const registerUser = async (
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
name: string,
|
||||
className: string,
|
||||
token: string
|
||||
) => {
|
||||
return makePostRequest(
|
||||
"/auth/register",
|
||||
{ firstName, lastName, className, token },
|
||||
{ name, className, token },
|
||||
"É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 ===
|
||||
|
|
@ -104,6 +109,8 @@ const defaultUser = {
|
|||
|
||||
export type User = typeof defaultUser;
|
||||
|
||||
export const AUTH_ERROR = "Unauthorized access"
|
||||
|
||||
export const useUser = () => {
|
||||
const { data, ...props } = useQuery({
|
||||
queryKey: ["user"],
|
||||
|
|
@ -122,8 +129,7 @@ export const useUser = () => {
|
|||
};
|
||||
|
||||
export const logout = async () => {
|
||||
// TODO: POST
|
||||
// TODO: Invalidate user query (cache)
|
||||
return makePostRequest("/auth/logout", {}, "Échec de la déconnexion", "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
export function extractLatexImages(html: string) {
|
||||
const imgRegex =
|
||||
/<img[^>]+src="(https:\/\/latex\.codecogs\.com\/gif\.latex\?(=?.*?))"[^>]*>/g;
|
||||
let parts = [];
|
||||
let latexMatches: string[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
html.replace(imgRegex, (match, _, latex, index) => {
|
||||
parts.push(html.slice(lastIndex, index)); // Add HTML before image
|
||||
latexMatches.push(decodeURIComponent(latex)); // Extract and decode LaTeX
|
||||
lastIndex = index + match.length;
|
||||
return "";
|
||||
});
|
||||
|
||||
parts.push(html.slice(lastIndex)); // Add remaining HTML after last image
|
||||
|
||||
return { parts, latexMatches };
|
||||
}
|
||||
|
||||
export function renderLatex(html: string) {
|
||||
const { parts, latexMatches } = extractLatexImages(html);
|
||||
const outputHtml = parts
|
||||
.map((part, i) => {
|
||||
if (!latexMatches[i]) {
|
||||
return part;
|
||||
}
|
||||
return `${part}$$${latexMatches[i]}$$`;
|
||||
})
|
||||
.join("");
|
||||
// Remove all "\," from string
|
||||
const regex = /\\,/g;
|
||||
return outputHtml.replace(regex, " ");
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import "./app.css";
|
|||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import queryClient from "~/lib/client";
|
||||
import Toaster from "~/components/ui/sonner";
|
||||
import { ThemeProvider } from "./components/theme-provider";
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
|
|
@ -51,7 +52,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||
</head>
|
||||
<body>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
<Toaster />
|
||||
<ScrollRestoration />
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import "katex/dist/katex.min.css";
|
||||
import { DomUtils, parseDocument } from "htmlparser2";
|
||||
// TODO: API - remove trailing lines from HTML comment/content
|
||||
// TODO: Server side image extraction and latex rendering
|
||||
// TEMP SOLUTION
|
||||
import { renderLatex } from "~/lib/latex"; // Custom LaTeX rendering function
|
||||
// function removeTrailingLines(htmlString: string) {
|
||||
// return htmlString.replace(/(<br\s*\/?>\s*)+$/gi, "").trim();
|
||||
// }
|
||||
|
||||
import Latex from "react-latex-next";
|
||||
import { useState } from "react";
|
||||
import { Navigate, useNavigate, useParams } from "react-router";
|
||||
|
|
@ -37,7 +29,7 @@ import AttachmentItem from "~/components/details/attachment";
|
|||
// import { ScrollToTopOnMount } from "~/components/noscroll";
|
||||
import Error from "~/components/error";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { useColle, useUser } from "~/lib/api";
|
||||
import { AUTH_ERROR, useColle, useUser } from "~/lib/api";
|
||||
import { toast } from "sonner";
|
||||
import { formatDate, formatGrade, formatTime, goBack } from "~/lib/utils";
|
||||
|
||||
|
|
@ -60,10 +52,14 @@ export default function ColleDetailPage() {
|
|||
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
|
||||
// TODO: Handle user loading state
|
||||
if (isUserLoading) {
|
||||
return <ColleDetailsSkeleton />;
|
||||
}
|
||||
if (userError?.message === AUTH_ERROR) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
if (userError) {
|
||||
console.error(userError);
|
||||
return <Navigate to="/login" />;
|
||||
return <Error message={userError.message} />;
|
||||
}
|
||||
|
||||
// TODO: Favorite toggle function
|
||||
|
|
@ -84,7 +80,6 @@ export default function ColleDetailPage() {
|
|||
description="Une erreur s'est produite lors du chargement de la colle."
|
||||
/>
|
||||
);
|
||||
|
||||
if (isLoading || !colle) return <ColleDetailsSkeleton />;
|
||||
|
||||
const handleToggleFavorite = () => {};
|
||||
|
|
@ -432,7 +427,7 @@ function ExpandableComment({ comment }: { comment: string }) {
|
|||
return (
|
||||
<div>
|
||||
<Latex delimiters={[{ left: "$$", right: "$$", display: false }]}>
|
||||
{renderLatex(displayedComment)}
|
||||
{displayedComment}
|
||||
</Latex>
|
||||
{isLongComment && (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
import { Navigate } from "react-router";
|
||||
import Error from "~/components/error";
|
||||
import HomePage from "~/components/home";
|
||||
import Loader from "~/components/loader";
|
||||
import UserDropdown from "~/components/user-dropdown";
|
||||
import { MainLayout } from "~/layout";
|
||||
import { useUser } from "~/lib/api";
|
||||
import { AUTH_ERROR, useUser } from "~/lib/api";
|
||||
import { forceReload } from "~/lib/utils";
|
||||
|
||||
export default function Home() {
|
||||
const { user, isLoading, error } = useUser();
|
||||
if (error) {
|
||||
console.error(error);
|
||||
// TODO: handle error (redirect to login or show error message)
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
if (error?.message === AUTH_ERROR) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
if (error) {
|
||||
return <Error message={error.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MainLayout page={<HomePage />}>
|
||||
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
||||
Khollisé - {user.className} ⚔️
|
||||
</h1>
|
||||
{/* TODO: isLoading to display skeleton */}
|
||||
<UserDropdown user={user} />
|
||||
</MainLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
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 { Input } from "~/components/ui/input";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AuthLayout } from "~/layout";
|
||||
import { AuthLayout, SUPPORT_MAIL } from "~/layout";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { capitalizeFirstLetter } from "~/lib/utils";
|
||||
import { Combobox } from "~/components/combobox";
|
||||
import { getClasses, registerUser } from "~/lib/api";
|
||||
import { getClasses, getNames, registerUser } from "~/lib/api";
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
|
|
@ -23,9 +29,8 @@ export default function Register() {
|
|||
const [searchParams] = useSearchParams();
|
||||
const email = searchParams.get("email")!;
|
||||
const [emailFirst, emailLast] = getNameFromEmail(email);
|
||||
const [firstName, setFirstName] = useState(emailFirst);
|
||||
const [lastName, setLastName] = useState(emailLast);
|
||||
const [className, setClassName] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const token = searchParams.get("token");
|
||||
useEffect(() => {
|
||||
|
|
@ -35,40 +40,69 @@ export default function Register() {
|
|||
}
|
||||
}, [email, token]);
|
||||
|
||||
const [classes, setClasses] = useState<{ value: string; label: string }[]>([]);
|
||||
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);
|
||||
|
||||
const [names, setNames] = useState<{ value: string; label: string }[]>([]);
|
||||
const hasSubmitted = names.length > 0;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
// 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);
|
||||
if (!hasSubmitted) {
|
||||
// Validate inputs
|
||||
if (!className) return setError("Veuillez sélectionner votre classe.");
|
||||
setIsLoading(true);
|
||||
|
||||
await registerUser(firstName, lastName, className, token!)
|
||||
.then(() => {
|
||||
setIsLoading(false);
|
||||
navigate("/", {
|
||||
replace: true,
|
||||
await getNames(className)
|
||||
.then((names) => {
|
||||
setNames(names);
|
||||
setIsLoading(false);
|
||||
|
||||
// Try to default name from email
|
||||
const possibleName = names.find(
|
||||
(n: { label: string }) => n.label === `${emailFirst} ${emailLast}`
|
||||
);
|
||||
if (possibleName) {
|
||||
setName(possibleName.value);
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err.message);
|
||||
});
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
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 (
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
|
|
@ -87,32 +121,6 @@ export default function Register() {
|
|||
</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">
|
||||
<Label htmlFor="password">Classe</Label>
|
||||
<div className="block">
|
||||
|
|
@ -123,10 +131,34 @@ export default function Register() {
|
|||
placeholderText="Rechercher"
|
||||
current={className}
|
||||
setValue={setClassName}
|
||||
disabled={names.length > 0}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{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 && (
|
||||
<Alert variant="destructive" className="pb-2">
|
||||
<AlertCircleIcon className="h-4 w-4" />
|
||||
|
|
@ -138,12 +170,16 @@ export default function Register() {
|
|||
{isLoading ? (
|
||||
<>
|
||||
<LoaderCircle className="h-4 w-4 animate-spin mr-2" />
|
||||
Inscription en cours...
|
||||
Validation en cours...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn className="h-4 w-4 mr-2" />
|
||||
S'inscrire
|
||||
{hasSubmitted ? (
|
||||
<LogIn className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<ArrowRight className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{hasSubmitted ? "S'inscrire" : "Continuer"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue