Added ui & auth data
This commit is contained in:
parent
bd8418b86a
commit
bb172cae65
17 changed files with 294 additions and 104 deletions
|
@ -1,4 +1,5 @@
|
|||
import Leaderboard from '@/ui/Leaderboard';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Tableau des scores - Peer-at Code',
|
||||
|
@ -6,5 +7,6 @@ export const metadata = {
|
|||
};
|
||||
|
||||
export default async function Page() {
|
||||
return <Leaderboard />;
|
||||
const token = cookies().get('token')?.value;
|
||||
return <Leaderboard token={token!} />;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import Card from '@/ui/Card';
|
||||
'use client';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Dashboard - Peer-at Code'
|
||||
};
|
||||
import { useMe } from '@/lib/hooks/use-players';
|
||||
import Card from '@/ui/Card';
|
||||
import cookies from 'js-cookie';
|
||||
|
||||
export default function Page() {
|
||||
const token = cookies.get('token');
|
||||
const { data: me, isLoading } = useMe({ token: token! });
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col space-y-4">
|
||||
<div className="w-full">
|
||||
|
@ -14,9 +16,24 @@ export default function Page() {
|
|||
<p className="text-muted">Ceci est la page d'accueil du dashboard</p>
|
||||
</header>
|
||||
<main className="flex-col justify-between space-x-0 space-y-4 md:flex md:flex-row md:space-x-6 md:space-y-0">
|
||||
<Card icon="pie-chart-line" title="46" data="Puzzles" />
|
||||
<Card icon="award-line" title="3" data="Badges" />
|
||||
<Card icon="bar-chart-line" title="10 ème" data="Classement" />
|
||||
<Card
|
||||
isLoading={isLoading}
|
||||
icon="pie-chart-line"
|
||||
title="Puzzles"
|
||||
data={me?.completions}
|
||||
/>
|
||||
<Card
|
||||
isLoading={isLoading}
|
||||
icon="award-line"
|
||||
title="Badges"
|
||||
data={me?.badges || 'Aucun'}
|
||||
/>
|
||||
<Card
|
||||
isLoading={isLoading}
|
||||
icon="bar-chart-line"
|
||||
title="Score (classement plus tard)"
|
||||
data={me?.score}
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { getPuzzle } from '@/lib/puzzles';
|
||||
import Puzzle from '@/ui/Puzzle';
|
||||
import type { Metadata } from 'next';
|
||||
|
@ -5,16 +6,26 @@ import { notFound } from 'next/navigation';
|
|||
|
||||
export async function generateMetadata({ params }: { params: { id: number } }): Promise<Metadata> {
|
||||
const { id } = params;
|
||||
const token = cookies().get('token')?.value;
|
||||
|
||||
const puzzle = await getPuzzle(id);
|
||||
if (!token) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const puzzle = await getPuzzle({ token, id });
|
||||
|
||||
return { title: `${puzzle.name} - Peer-at Code` };
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: { id: number } }) {
|
||||
const { id } = params;
|
||||
const token = cookies().get('token')?.value;
|
||||
|
||||
const puzzle = await getPuzzle(id);
|
||||
if (!token) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const puzzle = await getPuzzle({ token, id });
|
||||
|
||||
if (!puzzle) {
|
||||
notFound();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { cookies } from 'next/headers';
|
||||
|
||||
import Puzzles from '@/ui/Puzzles';
|
||||
|
||||
export const metadata = {
|
||||
|
@ -5,9 +7,12 @@ export const metadata = {
|
|||
};
|
||||
|
||||
export default async function Page() {
|
||||
const cookieStore = cookies();
|
||||
const token = cookieStore.get('token')?.value;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Puzzles />
|
||||
<Puzzles token={token!} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import Console from '@/ui/Console';
|
|||
import Image from 'next/image';
|
||||
|
||||
export default function Page() {
|
||||
// TODO: Fix this (image)
|
||||
return (
|
||||
<div>
|
||||
<div className="flex h-screen w-full">
|
||||
|
|
|
@ -6,4 +6,16 @@
|
|||
.console {
|
||||
@apply relative top-0.5 inline-block;
|
||||
}
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0px 1000px hsl(258deg 15% 17%) inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,46 @@
|
|||
import BoringAvatar from 'boring-avatars';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Avatar({ name, size = 28 }: { name: string; size?: number }) {
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Avatar({ name, size = 36 }: { name: string; size?: number }) {
|
||||
return <BoringAvatar name={name} variant="beam" size={size} />;
|
||||
}
|
||||
|
||||
export function Base64Avatar({
|
||||
name,
|
||||
src,
|
||||
className
|
||||
}: {
|
||||
name: string;
|
||||
src: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Image
|
||||
src={`data:image;base64,${src}`}
|
||||
className={cn('rounded-full object-cover', className)}
|
||||
width="0"
|
||||
height="0"
|
||||
alt={name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AvatarComponent({
|
||||
name,
|
||||
src,
|
||||
size = 36,
|
||||
className
|
||||
}: {
|
||||
name: string;
|
||||
src: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return src ? (
|
||||
<Base64Avatar name={name} src={src} className={className} />
|
||||
) : (
|
||||
<Avatar name={name} size={size} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,12 @@ import { cn } from '@/lib/utils';
|
|||
|
||||
export type Difficulty = 'easy' | 'medium' | 'hard';
|
||||
|
||||
export const DIFFICULTY = {
|
||||
1: 'easy',
|
||||
2: 'medium',
|
||||
3: 'hard'
|
||||
}
|
||||
|
||||
export default function Badge({
|
||||
title,
|
||||
path,
|
||||
|
|
|
@ -10,11 +10,12 @@ const Button = forwardRef<
|
|||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md border-0 px-5 py-2.5 text-center text-sm font-medium outline-none transition-colors focus:outline-none focus:ring-0 disabled:opacity-50',
|
||||
'inline-flex items-center justify-center rounded-md border-0 px-5 py-2.5 text-center text-sm font-medium outline-none transition-colors focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
{
|
||||
'bg-highlight-primary hover:bg-highlight-primary/60': kind === 'default',
|
||||
'bg-error hover:bg-error/60': kind === 'danger',
|
||||
'bg-gradient-to-tl from-brand to-brand-accent hover:bg-opacity-80': kind === 'brand'
|
||||
'bg-gradient-to-tl from-brand to-brand-accent transition-opacity hover:opacity-90':
|
||||
kind === 'brand'
|
||||
},
|
||||
className
|
||||
)}
|
||||
|
|
27
ui/Card.tsx
27
ui/Card.tsx
|
@ -1,12 +1,33 @@
|
|||
import Icon from './Icon';
|
||||
|
||||
export default function Card({ icon, title, data }: { icon: string; title: string; data: string }) {
|
||||
export default function Card({
|
||||
isLoading,
|
||||
icon,
|
||||
title,
|
||||
data
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
icon: string;
|
||||
title: string;
|
||||
data: any;
|
||||
}) {
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md">
|
||||
<Icon name={icon} className="text-2xl text-muted" />
|
||||
<div className="flex flex-col space-y-4">
|
||||
<span className="h-4 w-32 animate-pulse rounded bg-highlight-primary" />
|
||||
<span className="h-4 w-24 animate-pulse rounded bg-highlight-primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md">
|
||||
<Icon name={icon} className="text-2xl text-muted" />
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-xl font-semibold">{title}</h3>
|
||||
<p className="text-muted">{data}</p>
|
||||
<h3 className="text-xl font-semibold">{data}</h3>
|
||||
<p className="text-muted">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { forwardRef } from 'react';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import Icon from './Icon';
|
||||
import Label from './Label';
|
||||
|
||||
const Input = forwardRef<
|
||||
|
@ -14,7 +15,8 @@ const Input = forwardRef<
|
|||
<Label label={label} description={description} required={props.required} className={className}>
|
||||
<input
|
||||
ref={ref}
|
||||
className="w-full rounded-md border border-primary-600 bg-highlight-primary px-5 py-2.5 text-sm font-medium focus:border-brand focus:bg-primary-800 focus:outline-none disabled:opacity-50"
|
||||
className="w-full rounded-md border-primary-600 bg-highlight-primary px-5 py-2.5 text-sm font-medium outline-0 focus:border-brand focus:bg-primary-800 focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50"
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
/>
|
||||
</Label>
|
||||
|
|
|
@ -2,52 +2,78 @@
|
|||
|
||||
import { useLeaderboard } from '@/lib/hooks/use-leaderboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Avatar from './Avatar';
|
||||
import { useMemo, useState } from 'react';
|
||||
import AvatarComponent from './Avatar';
|
||||
import Select from './Select';
|
||||
|
||||
// TODO: Generate this later
|
||||
const scoreColors = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
|
||||
|
||||
// TODO: Generate this later
|
||||
export const options = [
|
||||
{ value: '1i1', title: '1I1' },
|
||||
{ value: '1i2', title: '1I2' },
|
||||
{ value: '1i3', title: '1I3' },
|
||||
{ value: '1i4', title: '1I4' },
|
||||
{ value: '1i5', title: '1I5' },
|
||||
{ value: '1i6', title: '1I6' },
|
||||
{ value: '1i7', title: '1I7' },
|
||||
{ value: '1i8', title: '1I8' }
|
||||
];
|
||||
export default function Leaderboard({ token }: { token: string }) {
|
||||
const { data, isLoading } = useLeaderboard({ token });
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
let options;
|
||||
|
||||
if (data) {
|
||||
options = data
|
||||
.filter((score, index, self) => {
|
||||
return index === self.findIndex((t) => t.group === score.group) && score.group !== '';
|
||||
})
|
||||
.sort((a, b) => (a.group > b.group ? 1 : -1))
|
||||
.map((score) => ({ value: score.group, title: score.group }));
|
||||
options.unshift({ value: '', title: 'Tous' });
|
||||
options.push({ value: 'no-group', title: 'Sans groupe' });
|
||||
}
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (filter) {
|
||||
if (filter === 'no-group') {
|
||||
return data?.filter((score) => score.group === '');
|
||||
}
|
||||
return data?.filter((score) => score.group === filter);
|
||||
}
|
||||
return data;
|
||||
}, [data, filter]);
|
||||
|
||||
export default function Leaderboard() {
|
||||
const { data, isLoading } = useLeaderboard();
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col space-y-4">
|
||||
<div className="w-full">
|
||||
<section className="flex flex-col space-y-4">
|
||||
<header className="flex items-center justify-between">
|
||||
<section className="flex h-full w-full flex-col space-y-4">
|
||||
<header className="sticky flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">Tableau des scores</h3>
|
||||
<p className="hidden text-muted sm:block">
|
||||
Suivez la progression des élèves en direct
|
||||
</p>
|
||||
<p className="hidden text-muted sm:block">Suivez la progression des élèves en direct</p>
|
||||
</div>
|
||||
<Select className="w-28" options={options} />
|
||||
{(filteredData && (
|
||||
<Select
|
||||
className="w-32"
|
||||
options={options || []}
|
||||
value={filter}
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
)) || (
|
||||
<span
|
||||
className="inline-block h-12 w-32 animate-pulse rounded-lg bg-primary-600"
|
||||
style={{
|
||||
animationDuration: '1s'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
||||
<ul className="flex flex-col space-y-2">
|
||||
{(!isLoading &&
|
||||
data?.map((score, key) => (
|
||||
<div key={key} className="flex flex-col space-y-2">
|
||||
<div className="flex justify-between space-x-2">
|
||||
filteredData?.map((score, key) => (
|
||||
<li key={key} className="flex justify-between space-x-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className={cn('font-semibold', scoreColors[key])}>{key + 1}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar name={score.pseudo} />
|
||||
<AvatarComponent name={score.pseudo} src={score.avatar} className="h-9 w-9" />
|
||||
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
|
||||
<span className="text-lg">{score.pseudo}</span>
|
||||
<span className="text-sm text-muted">{score.group}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">Puzzles</span>
|
||||
|
@ -58,8 +84,7 @@ export default function Leaderboard() {
|
|||
<span className="text-lg text-muted">{score.score}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))) ||
|
||||
[...Array(20).keys()].map((i) => (
|
||||
<span
|
||||
|
@ -71,9 +96,8 @@ export default function Leaderboard() {
|
|||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,12 +73,14 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
|
|||
label="Réponse"
|
||||
type="text"
|
||||
placeholder="12"
|
||||
required
|
||||
{...register('answer')}
|
||||
/>
|
||||
<Input
|
||||
className="h-16 w-full sm:w-1/3"
|
||||
label="Code"
|
||||
type="file"
|
||||
required
|
||||
accept=".py,.js,.ts,.java,.rust,.c"
|
||||
{...register('code_file')}
|
||||
/>
|
||||
|
|
|
@ -4,8 +4,9 @@ import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
|||
import AppLink from './AppLink';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default function Puzzles() {
|
||||
const { data, isLoading } = usePuzzles();
|
||||
export default function Puzzles({ token }: { token: string }) {
|
||||
const { data, isLoading } = usePuzzles({ token });
|
||||
console.log(data);
|
||||
return (
|
||||
<>
|
||||
{(!isLoading &&
|
||||
|
@ -48,7 +49,7 @@ export default function Puzzles() {
|
|||
/>
|
||||
</div>
|
||||
<ul className="flex flex-col space-y-4">
|
||||
{[...Array(7).keys()].map((j) => (
|
||||
{[...Array(6).keys()].map((j) => (
|
||||
<span
|
||||
key={j}
|
||||
className="inline-block h-14 animate-pulse rounded-lg bg-primary-600"
|
||||
|
|
|
@ -10,7 +10,8 @@ const Select = forwardRef<
|
|||
error?: React.ReactNode;
|
||||
description?: string;
|
||||
options: { value: string; title: string }[];
|
||||
} & Partial<ReturnType<UseFormRegister<any>>>
|
||||
}
|
||||
//& Partial<ReturnType<UseFormRegister<any>>></HTMLSelectElement>
|
||||
>(({ options, className, label, description, error, ...props }, ref) => (
|
||||
<>
|
||||
<Label label={label} description={description} required={props.required} className={className}>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import cookies from 'js-cookie';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import AppLink from './AppLink';
|
||||
import Button from './Button';
|
||||
|
@ -37,13 +37,14 @@ export default function UserAuthForm() {
|
|||
avatar: ''
|
||||
}
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname()!;
|
||||
const isSignIn = pathname.includes('sign-in');
|
||||
const token = cookies.get('token');
|
||||
|
||||
async function onSubmit(data: FormData) {
|
||||
setIsLoading(true);
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/${isSignIn ? 'login' : 'register'}`,
|
||||
{
|
||||
|
@ -59,18 +60,21 @@ export default function UserAuthForm() {
|
|||
type: 'manual',
|
||||
message: "Nom d'utilisateur indisponible"
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
if (!email_valid) {
|
||||
setError('email', {
|
||||
type: 'manual',
|
||||
message: 'Email déjà utilisé'
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
const token = res.headers.get('Authorization')?.split(' ')[1];
|
||||
if (token) cookies.set('token', token);
|
||||
router.refresh();
|
||||
} else {
|
||||
setError('passwd', {
|
||||
type: 'manual',
|
||||
|
@ -79,10 +83,6 @@ export default function UserAuthForm() {
|
|||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (token) router.push('/dashboard');
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex w-52 flex-col justify-center space-y-4 sm:w-72"
|
||||
|
@ -100,14 +100,14 @@ export default function UserAuthForm() {
|
|||
/>
|
||||
<Input
|
||||
label="Nom"
|
||||
type="lastname"
|
||||
type="text"
|
||||
placeholder="Doe"
|
||||
error={errors.lastname?.message}
|
||||
{...register('lastname')}
|
||||
/>
|
||||
<Input
|
||||
label="Prénom"
|
||||
type="firstname"
|
||||
type="text"
|
||||
placeholder="John"
|
||||
error={errors.firstname?.message}
|
||||
{...register('firstname')}
|
||||
|
@ -130,7 +130,7 @@ export default function UserAuthForm() {
|
|||
error={errors.passwd?.message}
|
||||
{...register('passwd')}
|
||||
/>
|
||||
<Button type="submit" kind="brand">
|
||||
<Button type="submit" kind="brand" disabled={isLoading}>
|
||||
{isSignIn ? 'Se connecter' : "S'inscrire"}
|
||||
</Button>
|
||||
<div className="flex flex-col text-center">
|
||||
|
|
|
@ -1,11 +1,34 @@
|
|||
'use client';
|
||||
|
||||
import { useMe } from '@/lib/hooks/use-players';
|
||||
import { titleCase } from '@/lib/utils';
|
||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||
import cookies from 'js-cookie';
|
||||
import { useRouter, useSelectedLayoutSegment } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import AvatarComponent from '../Avatar';
|
||||
import Icon from '../Icon';
|
||||
import Popover from '../Popover';
|
||||
|
||||
export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const segment = useSelectedLayoutSegment();
|
||||
|
||||
const token = cookies.get('token');
|
||||
|
||||
const { data: me, isLoading } = useMe({ token: token! });
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
async function handleLogout() {
|
||||
cookies.remove('token');
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-50 flex w-full flex-row items-center justify-between border-b border-solid border-highlight-primary bg-secondary py-4 px-8">
|
||||
<div className="flex flex-row items-center space-x-2 sm:space-x-0">
|
||||
|
@ -21,12 +44,34 @@ export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
|||
)}
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<button className="flex items-center text-2xl text-error">
|
||||
<Icon name="flag-line" />
|
||||
{!isLoading && me ? (
|
||||
<Popover
|
||||
open={isMenuOpen}
|
||||
onOpenChange={setIsMenuOpen}
|
||||
trigger={
|
||||
<button className="mx-auto flex items-center gap-2">
|
||||
<AvatarComponent name={me.pseudo} src={me.avatar} className="h-9 w-9" />
|
||||
<span>{me?.pseudo}</span>
|
||||
</button>
|
||||
<button className="flex items-center justify-center rounded-full border border-primary-400 bg-tertiary px-4 py-2">
|
||||
T
|
||||
}
|
||||
>
|
||||
<nav className="flex w-32 flex-col gap-2">
|
||||
<button
|
||||
className="flex items-center gap-1 p-2 text-error hover:bg-error/10"
|
||||
onClick={() => handleLogout()}
|
||||
>
|
||||
Se déconnecter
|
||||
</button>
|
||||
</nav>
|
||||
</Popover>
|
||||
) : (
|
||||
<div className="animate-pulse">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-9 w-9 rounded-full bg-highlight-primary" />
|
||||
<div className="h-4 w-14 rounded-full bg-highlight-primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue