Compare commits
No commits in common. "0684a0649bb270eb1398853d27bf0e1caa869fe6" and "85e2552db871ededd650483bba88bf708bedf4a6" have entirely different histories.
0684a0649b
...
85e2552db8
12 changed files with 152 additions and 292 deletions
|
|
@ -22,9 +22,7 @@ export function Combobox({
|
||||||
defaultText = "Select value...",
|
defaultText = "Select value...",
|
||||||
placeholderText = "Search...",
|
placeholderText = "Search...",
|
||||||
emptyText = "No value found.",
|
emptyText = "No value found.",
|
||||||
current,
|
current, setValue
|
||||||
setValue,
|
|
||||||
disabled = false,
|
|
||||||
}: {
|
}: {
|
||||||
values?: { value: string; label: string }[]
|
values?: { value: string; label: string }[]
|
||||||
emptyText?: string
|
emptyText?: string
|
||||||
|
|
@ -32,19 +30,17 @@ 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 && !disabled} onOpenChange={setOpen}>
|
<Popover open={open} 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!
|
||||||
|
|
|
||||||
|
|
@ -79,17 +79,6 @@ export default function Home() {
|
||||||
isLoading,
|
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
|
// TODO: FAVORITES
|
||||||
const useToggleStar = (auth: any) => {};
|
const useToggleStar = (auth: any) => {};
|
||||||
// TODO: FILTERS
|
// TODO: FILTERS
|
||||||
|
|
@ -138,6 +127,17 @@ export default function Home() {
|
||||||
// setTimeout(restoreScrollPosition, 500);
|
// setTimeout(restoreScrollPosition, 500);
|
||||||
// }, [location]);
|
// }, [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 (
|
return (
|
||||||
<div className="space-y-6 pb-20 md:pb-0">
|
<div className="space-y-6 pb-20 md:pb-0">
|
||||||
{/* Week Navigation */}
|
{/* Week Navigation */}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
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,32 +17,25 @@ import {
|
||||||
Settings,
|
Settings,
|
||||||
Sun,
|
Sun,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useDisplayedTheme } from "~/components/theme-provider";
|
// TODO: THEME
|
||||||
|
// import { useTheme } from "~/components/ui/theme-provider";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { logout, type User } from "~/lib/api";
|
import { logout, type User } from "~/lib/api";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export default function UserDropdown({ user }: { user: User }) {
|
export default function UserDropdown({ user }: { user: User }) {
|
||||||
const { theme, setTheme } = useDisplayedTheme();
|
// TODO: const { theme, setTheme } = useTheme();
|
||||||
|
const [theme, setTheme] = useState("light");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const useLogout = () => {
|
const handleLogout = async () => {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
// Logout, invalidate user cache, and redirect to login
|
|
||||||
return async () => {
|
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
queryClient.removeQueries({ queryKey: ["user"] });
|
|
||||||
navigate("/login", { replace: true });
|
navigate("/login", { replace: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout failed:", error);
|
console.error("Logout failed:", error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = useLogout();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
|
|
@ -64,7 +57,9 @@ export default function UserDropdown({ user }: { user: User }) {
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
<div className="flex flex-col space-y-1">
|
<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">
|
<p className="text-xs leading-none text-muted-foreground">
|
||||||
{user.className}
|
{user.className}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -92,20 +87,23 @@ export default function UserDropdown({ user }: { user: User }) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate("/settings");
|
navigate("/settings");
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
<span>Paramètres</span>
|
<span>
|
||||||
|
Paramètres
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={(e) => {
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate("/progress");
|
navigate("/progress");
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<LineChartIcon className="mr-2 h-4 w-4" />
|
<LineChartIcon className="mr-2 h-4 w-4" />
|
||||||
<span>Progression</span>
|
<span
|
||||||
|
|
||||||
|
>
|
||||||
|
Progression
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
export const SUPPORT_MAIL = "mailto:" + import.meta.env.VITE_SUPPORT_EMAIL;
|
const SUPPORT_MAIL = "mailto:" + import.meta.env.VITE_SUPPORT_EMAIL;
|
||||||
|
|
||||||
export function AuthLayout({
|
export function AuthLayout({
|
||||||
children,
|
children,
|
||||||
|
|
|
||||||
|
|
@ -70,23 +70,18 @@ export const getClasses = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const registerUser = async (
|
export const registerUser = async (
|
||||||
name: string,
|
firstName: string,
|
||||||
|
lastName: string,
|
||||||
className: string,
|
className: string,
|
||||||
token: string
|
token: string
|
||||||
) => {
|
) => {
|
||||||
return makePostRequest(
|
return makePostRequest(
|
||||||
"/auth/register",
|
"/auth/register",
|
||||||
{ name, className, token },
|
{ firstName, lastName, 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 ===
|
||||||
|
|
@ -109,8 +104,6 @@ const defaultUser = {
|
||||||
|
|
||||||
export type User = typeof defaultUser;
|
export type User = typeof defaultUser;
|
||||||
|
|
||||||
export const AUTH_ERROR = "Unauthorized access"
|
|
||||||
|
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
const { data, ...props } = useQuery({
|
const { data, ...props } = useQuery({
|
||||||
queryKey: ["user"],
|
queryKey: ["user"],
|
||||||
|
|
@ -129,7 +122,8 @@ export const useUser = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logout = async () => {
|
export const logout = async () => {
|
||||||
return makePostRequest("/auth/logout", {}, "Échec de la déconnexion", "POST");
|
// TODO: POST
|
||||||
|
// TODO: Invalidate user query (cache)
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
33
app/lib/latex.ts
Normal file
33
app/lib/latex.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
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,7 +12,6 @@ import "./app.css";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import queryClient from "~/lib/client";
|
import queryClient from "~/lib/client";
|
||||||
import Toaster from "~/components/ui/sonner";
|
import Toaster from "~/components/ui/sonner";
|
||||||
import { ThemeProvider } from "./components/theme-provider";
|
|
||||||
|
|
||||||
export const links: Route.LinksFunction = () => [
|
export const links: Route.LinksFunction = () => [
|
||||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
|
|
@ -52,7 +51,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
{children}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import { DomUtils, parseDocument } from "htmlparser2";
|
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 Latex from "react-latex-next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Navigate, useNavigate, useParams } from "react-router";
|
import { Navigate, useNavigate, useParams } from "react-router";
|
||||||
|
|
@ -29,7 +37,7 @@ import AttachmentItem from "~/components/details/attachment";
|
||||||
// import { ScrollToTopOnMount } from "~/components/noscroll";
|
// import { ScrollToTopOnMount } from "~/components/noscroll";
|
||||||
import Error from "~/components/error";
|
import Error from "~/components/error";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { AUTH_ERROR, useColle, useUser } from "~/lib/api";
|
import { useColle, useUser } from "~/lib/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatDate, formatGrade, formatTime, goBack } from "~/lib/utils";
|
import { formatDate, formatGrade, formatTime, goBack } from "~/lib/utils";
|
||||||
|
|
||||||
|
|
@ -52,14 +60,10 @@ export default function ColleDetailPage() {
|
||||||
|
|
||||||
const [isReloading, setIsReloading] = useState(false);
|
const [isReloading, setIsReloading] = useState(false);
|
||||||
|
|
||||||
if (isUserLoading) {
|
// TODO: Handle user loading state
|
||||||
return <ColleDetailsSkeleton />;
|
|
||||||
}
|
|
||||||
if (userError?.message === AUTH_ERROR) {
|
|
||||||
return <Navigate to="/login" replace />;
|
|
||||||
}
|
|
||||||
if (userError) {
|
if (userError) {
|
||||||
return <Error message={userError.message} />;
|
console.error(userError);
|
||||||
|
return <Navigate to="/login" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Favorite toggle function
|
// TODO: Favorite toggle function
|
||||||
|
|
@ -80,6 +84,7 @@ export default function ColleDetailPage() {
|
||||||
description="Une erreur s'est produite lors du chargement de la colle."
|
description="Une erreur s'est produite lors du chargement de la colle."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading || !colle) return <ColleDetailsSkeleton />;
|
if (isLoading || !colle) return <ColleDetailsSkeleton />;
|
||||||
|
|
||||||
const handleToggleFavorite = () => {};
|
const handleToggleFavorite = () => {};
|
||||||
|
|
@ -427,7 +432,7 @@ function ExpandableComment({ comment }: { comment: string }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Latex delimiters={[{ left: "$$", right: "$$", display: false }]}>
|
<Latex delimiters={[{ left: "$$", right: "$$", display: false }]}>
|
||||||
{displayedComment}
|
{renderLatex(displayedComment)}
|
||||||
</Latex>
|
</Latex>
|
||||||
{isLongComment && (
|
{isLongComment && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,21 @@
|
||||||
import { Navigate } from "react-router";
|
|
||||||
import Error from "~/components/error";
|
|
||||||
import HomePage from "~/components/home";
|
import HomePage from "~/components/home";
|
||||||
import Loader from "~/components/loader";
|
|
||||||
import UserDropdown from "~/components/user-dropdown";
|
import UserDropdown from "~/components/user-dropdown";
|
||||||
import { MainLayout } from "~/layout";
|
import { MainLayout } from "~/layout";
|
||||||
import { AUTH_ERROR, useUser } from "~/lib/api";
|
import { useUser } from "~/lib/api";
|
||||||
import { forceReload } from "~/lib/utils";
|
import { forceReload } from "~/lib/utils";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user, isLoading, error } = useUser();
|
const { user, isLoading, error } = useUser();
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loader />;
|
|
||||||
}
|
|
||||||
if (error?.message === AUTH_ERROR) {
|
|
||||||
return <Navigate to="/login" replace />;
|
|
||||||
}
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Error message={error.message} />;
|
console.error(error);
|
||||||
|
// TODO: handle error (redirect to login or show error message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout page={<HomePage />}>
|
<MainLayout page={<HomePage />}>
|
||||||
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
||||||
Khollisé - {user.className} ⚔️
|
Khollisé - {user.className} ⚔️
|
||||||
</h1>
|
</h1>
|
||||||
|
{/* TODO: isLoading to display skeleton */}
|
||||||
<UserDropdown user={user} />
|
<UserDropdown user={user} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
import { Alert, AlertDescription } from "~/components/ui/alert";
|
import { Alert, AlertDescription } from "~/components/ui/alert";
|
||||||
import {
|
import { AlertCircleIcon, IdCardIcon, LoaderCircle, LogIn, MailIcon } from "lucide-react";
|
||||||
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, SUPPORT_MAIL } from "~/layout";
|
import { AuthLayout } 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, getNames, registerUser } from "~/lib/api";
|
import { getClasses, registerUser } from "~/lib/api";
|
||||||
|
|
||||||
export function meta() {
|
export function meta() {
|
||||||
return [
|
return [
|
||||||
|
|
@ -29,8 +23,9 @@ 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(() => {
|
||||||
|
|
@ -40,69 +35,40 @@ 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);
|
||||||
|
|
||||||
if (!hasSubmitted) {
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
|
if (!firstName || !lastName) return setError("Veuillez compléter votre prénom et nom.");
|
||||||
if (!className) return setError("Veuillez sélectionner votre classe.");
|
if (!className) return setError("Veuillez sélectionner votre classe.");
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
await getNames(className)
|
await registerUser(firstName, lastName, className, token!)
|
||||||
.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);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Register user
|
|
||||||
if (!name) return setError("Veuillez compléter votre nom.");
|
|
||||||
await registerUser(name, className, token!)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
navigate("/", {
|
navigate("/", {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout
|
<AuthLayout title="Inscription" description="Créez votre compte pour accéder à Khollisé.">
|
||||||
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>
|
||||||
|
|
@ -121,6 +87,32 @@ 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">
|
||||||
|
|
@ -131,33 +123,9 @@ export default function Register() {
|
||||||
placeholderText="Rechercher"
|
placeholderText="Rechercher"
|
||||||
current={className}
|
current={className}
|
||||||
setValue={setClassName}
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert variant="destructive" className="pb-2">
|
<Alert variant="destructive" className="pb-2">
|
||||||
|
|
@ -170,16 +138,12 @@ 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" />
|
||||||
Validation en cours...
|
Inscription en cours...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{hasSubmitted ? (
|
|
||||||
<LogIn className="h-4 w-4 mr-2" />
|
<LogIn className="h-4 w-4 mr-2" />
|
||||||
) : (
|
S'inscrire
|
||||||
<ArrowRight className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{hasSubmitted ? "S'inscrire" : "Continuer"}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue