Added Dashboard page back

This commit is contained in:
Théo 2023-05-01 16:43:31 +02:00
parent 8e15b1793b
commit a84a70fdc2
12 changed files with 166 additions and 197 deletions

View file

@ -1,104 +1,99 @@
'use client'; 'use client';
import { useContext } from 'react';
import { UserContext } from '@/context/user'; import { UserContext } from '@/context/user';
import Card from '@/ui/Card'; import Card from '@/ui/Card';
import { useContext } from 'react';
export default function Page() { export default function Page() {
const { data: me, isLoading } = useContext(UserContext); const { data: me, isLoading } = useContext(UserContext);
return ( return (
<div className="flex h-full w-full flex-col space-y-4"> <section className="w-full flex-col space-y-4">
<div className="w-full">
<section className="flex flex-col space-y-4">
<header> <header>
<h3 className="text-xl font-semibold">Tableau de bord</h3> <h1 className="text-xl font-semibold">Tableau de bord</h1>
<p className="text-muted">Ceci est la page d&apos;accueil du dashboard</p> <p className="text-highlight-secondary">Ceci est la page d&apos;accueil du dashboard</p>
</header> </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"> <main className="flex-col space-y-4">
<div className="w-full flex-col justify-between space-x-0 space-y-4 md:flex md:flex-row md:space-x-6 md:space-y-0">
<Card <Card
isLoading={isLoading} isLoading={isLoading}
icon="pie-chart-line" icon="pie-chart-line"
title="Puzzles résolus" title="Puzzles résolus"
data={me?.completions || 0} data={me?.completions ?? 0}
link="/dashboard/puzzles"
/> />
<Card <Card
isLoading={isLoading} isLoading={isLoading}
icon="award-line" icon="award-line"
title="Badges obtenus" title="Badges obtenus"
data={me?.badges?.length || 'Aucun'} data={me?.badges?.length ?? 'Aucun'}
link="/dashboard/badges"
/> />
<Card <Card
isLoading={isLoading} isLoading={isLoading}
icon="bar-chart-line" icon="bar-chart-line"
title="Rang actuel" title="Rang actuel"
data={me?.rank || 'Non classé'} data={me?.rank ?? 'Non classé'}
link="/dashboard/leaderboard"
/> />
</div>
<div className="grid grid-cols-1 gap-4">
<div className="flex flex-col space-y-4">
<header>
<h2 className="text-lg font-semibold">Derniers puzzles</h2>
<p className="text-highlight-secondary">
Voici les derniers puzzles que vous avez résolus ou essayer de résoudres
</p>
</header>
<div className="h-full max-h-96 overflow-y-scroll rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md">
<ul className="flex flex-col space-y-2">
{me?.completionsList && me.completionsList.length > 0 ? (
me?.completionsList
.sort(
(a, b) =>
a.score - b.score ||
a.tries - b.tries ||
a.puzzleName.localeCompare(b.puzzleName)
)
.map((completion, key) => {
return (
<li key={key} className="flex justify-between space-x-2">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<span className="text-lg">{completion.puzzleName}</span>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex flex-col">
<span className="text-sm font-semibold">
Essai{completion.tries > 1 ? 's' : ''}
</span>
<span className="text-right text-lg text-highlight-secondary">
{completion.tries}
</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold">Score</span>
<span className="text-right text-lg text-highlight-secondary">
{completion.score}
</span>
</div>
</div>
</li>
);
})
) : (
<li className="m-auto flex items-center justify-center">
<span className="text-lg text-highlight-secondary">
{isLoading ? 'Chargement en cours...' : 'Aucun puzzles'}
</span>
</li>
)}
</ul>
</div>
</div>
</div>
</main> </main>
</section> </section>
</div>
<div className="h-full w-full flex-col justify-between space-x-0 space-y-4 md:flex md:flex-row md:space-x-6 md:space-y-0">
<section className="flex h-full w-full flex-col space-y-4">
<header>
<h3 className="text-xl font-semibold">Guides</h3>
</header>
<main className="h-full w-full flex-col justify-between space-x-0 space-y-4 rounded-lg border border-highlight-primary bg-primary-700 md:flex md:flex-row md:space-x-6 md:space-y-0">
Work in progress
</main>
</section>
<section className="flex h-full w-full flex-col space-y-4">
<header>
<h3 className="text-xl font-semibold">Historiques</h3>
</header>
<main className="h-full w-full flex-col justify-between space-x-0 space-y-4 rounded-lg border border-highlight-primary bg-primary-700 md:flex md:flex-row md:space-x-6 md:space-y-0">
Work in progress
</main>
</section>
</div>
{/* TODO fix ça c'est pas responsive */}
{/* <section className="flex-col space-y-4">
<header>
<h3 className="text-xl font-semibold">Statistiques</h3>
<p className="text-muted">Ceci est la page d&apos;accueil du dashboard</p>
</header>
<main className="flex-col justify-between space-x-0 space-y-4 sm:flex sm:flex-row sm:space-x-6 sm:space-y-0">
<CardTable
puzzles={[
{ name: 'Jour 0 | Save Conway Gadgetski', id: 1', content: '' },
{ name: 'Jour 1 | Next', id: 2', content: '' },
{ name: 'Jour 2 | Previous', id: '3', content: '' },
{ name: 'Jour 3 | Next 1 loop', id: '4', content: '' },
{ name: 'Jour 4 | Next no loop + recursion', id: '5', content: '' },
{ name: 'Jour 5 | N first rows', id: '6', content: '' },
{ name: 'Week-end | Game of Life', id: '7', content: '' },
{ name: 'Jour 0 | Save Conway Gadgetski', id: '1', content: '' },
{ name: 'Jour 1 | Next', id: '2', content: '' },
{ name: 'Jour 2 | Previous', id: '3', content: '' },
{ name: 'Jour 3 | Next 1 loop', id: '4', content: '' },
{ name: 'Jour 4 | Next no loop + recursion', id: '5', content: '' },
{ name: 'Jour 5 | N first rows', id: '6', content: '' },
{ name: 'Week-end | Game of Life', id: '7', content: '' },
{ name: 'Jour 0 | Save Conway Gadgetski', id: '1', content: '' },
{ name: 'Jour 1 | Next', id: '2', content: '' },
{ name: 'Jour 2 | Previous', id: '3', content: '' },
{ name: 'Jour 3 | Next 1 loop', id: '4', content: '' },
{ name: 'Jour 4 | Next no loop + recursion', id: '5', content: '' },
{ name: 'Jour 5 | N first rows', id: '6', content: '' },
{ name: 'Week-end | Game of Life', id: '7', content: '' }
]}
/>
<CardTable
puzzles={[
{ name: 'Jour 0 | Save Conway Gadgetski', id: '1', content: '' },
{ name: 'Jour 1 | Next', id: '2', content: '' },
{ name: 'Jour 2 | Previous', id: '3', content: '' },
{ name: 'Jour 3 | Next 1 loop', id: '4', content: '' },
{ name: 'Jour 4 | Next no loop + recursion', id: '5', content: '' },
{ name: 'Jour 5 | N first rows', id: '6', content: '' },
{ name: 'Week-end | Game of Life', id: '7', content: '' }
]}
/>
</main>
</section> */}
</div>
); );
} }

View file

@ -8,7 +8,7 @@ export default function Page() {
useEffect(() => { useEffect(() => {
router.push('/'); router.push('/');
}, []); }, [router]);
return <></>; return <></>;
} }

View file

@ -1,12 +0,0 @@
import axios from 'axios';
const fetcher = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
insecureHTTPParser: true
});
export default fetcher;

View file

@ -20,33 +20,33 @@ export type NavItem = {
* @type {NavItem[]} * @type {NavItem[]}
*/ */
export const navItems: NavItem[] = [ export const navItems: NavItem[] = [
// { {
// name: 'Dashboard', name: 'Dashboard',
// slug: '', slug: 'dashboard',
// icon: 'dashboard-line', icon: 'dashboard-line',
// disabled: false disabled: false
// }, },
{ {
name: 'Classement', name: 'Classement',
slug: 'leaderboard', slug: 'dashboard/leaderboard',
icon: 'line-chart-line', icon: 'line-chart-line',
disabled: false disabled: false
}, },
{ {
name: 'Puzzles', name: 'Puzzles',
slug: 'puzzles', slug: 'dashboard/puzzles',
icon: 'code-s-slash-line', icon: 'code-s-slash-line',
disabled: false disabled: false
}, },
{ {
name: 'Badges', name: 'Badges',
slug: 'badges', slug: 'dashboard/badges',
icon: 'award-fill', icon: 'award-fill',
disabled: false disabled: false
}, },
{ {
name: 'Paramètres', name: 'Paramètres',
slug: 'settings', slug: 'dashboard/settings',
icon: 'equalizer-line', icon: 'equalizer-line',
disabled: false disabled: false
} }

View file

@ -38,6 +38,7 @@ export type Player = {
tries: number; tries: number;
completions: number; completions: number;
rank: number; rank: number;
completionsList: Completion[];
badges: Badge[] | null; badges: Badge[] | null;
}; };
@ -46,3 +47,9 @@ export type Badge = {
level: number; level: number;
logo?: string; logo?: string;
}; };
export type Completion = {
puzzleName: string;
tries: number;
score: number;
};

View file

@ -1,23 +1,28 @@
import { getURL } from '@/lib/utils';
import AppLink from './AppLink';
import Icon from './Icon'; import Icon from './Icon';
export default function Card({ export default function Card({
isLoading, isLoading,
icon, icon,
title, title,
data data,
link
}: { }: {
isLoading: boolean; isLoading: boolean;
icon: string; icon: string;
title: string; title: string;
data: any; data: any;
link?: string;
}) { }) {
if (isLoading) if (isLoading)
return ( 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"> <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" /> <Icon name={icon} className="text-2xl text-muted" />
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<span className="h-4 w-32 animate-pulse rounded bg-highlight-primary" /> <span className="h-[18px] w-32 animate-pulse rounded bg-highlight-primary" />
<span className="h-4 w-24 animate-pulse rounded bg-highlight-primary" /> <span className="h-[18px] w-24 animate-pulse rounded bg-highlight-primary" />
</div> </div>
</div> </div>
); );
@ -25,10 +30,20 @@ export default function Card({
return ( 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"> <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" /> <Icon name={icon} className="text-2xl text-muted" />
<div className="flex flex-col"> <div className="flex w-full items-center justify-between">
<h3 className="text-xl font-semibold">{data}</h3> <div className="flex-col">
<h2 className="text-xl font-semibold">{data}</h2>
<p className="text-muted">{title}</p> <p className="text-muted">{title}</p>
</div> </div>
{link && (
<AppLink
className="text-highlight-secondary transition-colors duration-150 hover:text-brand"
href={getURL(link)}
>
<Icon name="arrow-right-line" />
</AppLink>
)}
</div>
</div> </div>
); );
} }

View file

@ -1,45 +0,0 @@
import { type Puzzle } from '@/lib/puzzles';
import AppLink from './AppLink';
export default function CardTable({ puzzles }: { puzzles: Puzzle[] }) {
return (
<></>
// <div className="relative flex h-96 w-full overflow-scroll">
// <table className="w-full table-auto border-collapse rounded-lg border-2 border-highlight-primary bg-primary-700 text-left text-sm text-muted shadow-md">
// {/* <thead className="z-1 sticky -top-1 bg-primary-600 text-xs uppercase text-white ">
// <tr>
// <th className="px-6 py-3">Exercice</th>
// <th className="px-6 py-3">Tentative</th>
// <th className="px-6 py-3">Score</th>
// <th className="px-6 py-3">Dernier essai</th>
// <th className="px-6 py-3">
// <span className="sr-only">Reprendre</span>
// </th>
// </tr>
// </thead> */}
// <tbody className="overflow-scroll">
// {puzzles.length &&
// puzzles.map((puzzle) => (
// <tr key={puzzle.id} className="bg-primary-700 hover:bg-primary-800 ">
// <th scope="row" className="whitespace-nowrap px-6 py-4 font-medium text-white">
// {puzzle.name}
// </th>
// <td className="px-6 py-4">30</td>
// <td className="px-6 py-4">300</td>
// <td className="px-6 py-4">10/10/2010</td>
// <td className="px-6 py-4 text-right">
// <AppLink
// href={`dashboard/puzzles/${puzzle.id}`}
// className="font-medium text-brand hover:underline"
// >
// Reprendre
// </AppLink>
// </td>
// </tr>
// ))}
// </tbody>
// </table>
// </div>
);
}

View file

@ -1,37 +1,40 @@
'use client'; 'use client';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import useSWRSubscription, { type SWRSubscription } from 'swr/subscription';
import { type ScoreEvent } from '@/lib/leaderboard'; import { useLeaderboardEvent } from '@/lib/hooks/use-leaderboard';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import Podium from '@/ui/events/podium/Podium'; import Podium from '@/ui/events/podium/Podium';
import { Timer } from './Timer'; import { Timer } from './Timer';
import { type ScoreEvent } from '@/lib/leaderboard';
import useSWRSubscription, { type SWRSubscription } from 'swr/subscription';
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400']; const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
export default function Leaderboard() { export default function Leaderboard({ token }: { token: string }) {
// TODO CHANGER CECI // TODO CHANGER CECI
const CHAPITRE_EVENT = 1; const CHAPITRE_EVENT = 1;
const subscription: SWRSubscription<string, ScoreEvent, Error> = (key, { next }) => { // const subscription: SWRSubscription<string, ScoreEvent, Error> = (key, { next }) => {
const socket = new WebSocket(key); // const socket = new WebSocket(key);
socket.addEventListener('message', (event) => { // socket.addEventListener('message', (event) => {
next(null, JSON.parse(event.data)); // next(null, JSON.parse(event.data));
}); // });
socket.addEventListener('error', (event) => { // socket.addEventListener('error', (event) => {
console.error(event); // console.error(event);
}); // });
return () => socket.close(); // return () => socket.close();
}; // };
const { data } = useSWRSubscription( // const { data } = useSWRSubscription(
`wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${CHAPITRE_EVENT}`, // `wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${CHAPITRE_EVENT}`,
subscription // subscription
); // );
const { data, isLoading } = useLeaderboardEvent({ token: token, id: CHAPITRE_EVENT });
const scores = [data?.groups] const scores = [data?.groups]
.flat() .flat()
@ -60,8 +63,8 @@ export default function Leaderboard() {
const tries = group.players.reduce((a, b) => a + b.tries, 0) || 0; const tries = group.players.reduce((a, b) => a + b.tries, 0) || 0;
return ( return (
<motion.li <motion.li
layout
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
// @ts-ignore TODO Je sais pas c'est quoi cette merde
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
@ -69,13 +72,18 @@ export default function Leaderboard() {
className="flex justify-between space-x-2" className="flex justify-between space-x-2"
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className={cn('font-semibold', SCORE_COLORS[group.rank - 1])}> <span
className={cn(
'font-semibold text-highlight-secondary',
SCORE_COLORS[group.rank - 1]
)}
>
{group.rank} {group.rank}
</span> </span>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center"> <div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
<span className="text-lg">{group.name}</span> <span className="text-lg">{group.name}</span>
<span className="text-sm text-muted"> <span className="text-sm text-highlight-secondary">
{group.players && group.players.length > 1 {group.players && group.players.length > 1
? group.players ? group.players
.map((player) => player.pseudo || 'Anonyme') .map((player) => player.pseudo || 'Anonyme')
@ -89,15 +97,15 @@ export default function Leaderboard() {
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{/* <div className="flex flex-col"> {/* <div className="flex flex-col">
<span className="text-sm font-semibold">Puzzle{puzzles > 1 ? 's' : ''}</span> <span className="text-sm font-semibold">Puzzle{puzzles > 1 ? 's' : ''}</span>
<span className="text-lg text-muted">{puzzles}</span> <span className="text-lg text-highlight-secondary">{puzzles}</span>
</div> */} </div> */}
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-semibold">Essai{tries > 1 ? 's' : ''}</span> <span className="text-sm font-semibold">Essai{tries > 1 ? 's' : ''}</span>
<span className="text-lg text-muted">{tries}</span> <span className="text-lg text-highlight-secondary">{tries}</span>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-semibold">Score</span> <span className="text-sm font-semibold">Score</span>
<span className="text-lg text-muted"> <span className="text-lg text-highlight-secondary">
{group.players.reduce((a, b) => a + b.score, 0)} {group.players.reduce((a, b) => a + b.score, 0)}
</span> </span>
</div> </div>

View file

@ -257,14 +257,14 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
{puzzle.tags {puzzle.tags
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name)) .filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
.map((tag, i) => ( .map((tag, i) => (
<span key={i} className="inline-block rounded-md bg-primary-900 px-2 py-1"> <span key={i} className="inline-block rounded-md bg-primary-800 px-2 py-1">
{tag.name} {tag.name}
</span> </span>
))} ))}
</div> </div>
)} )}
<Icon <Icon
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0" className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand"
name="arrow-right-line" name="arrow-right-line"
/> />
</div> </div>
@ -292,7 +292,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
</div> </div>
)} )}
<Icon <Icon
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0" className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0"
name="arrow-right-line" name="arrow-right-line"
/> />
</div> </div>

View file

@ -13,7 +13,7 @@ export default function ToHTML({ data, className }: { data: string; className?:
a: ({ node, ...props }) => ( a: ({ node, ...props }) => (
<a <a
{...props} {...props}
className="inline text-brand-accent hover:text-brand hover:underline" className="inline text-brand transition-colors duration-150 hover:text-brand-accent hover:underline"
// MAKE thIS SHIT DOWNLOADABLE // MAKE thIS SHIT DOWNLOADABLE
target="_blank" target="_blank"
rel="noopener" rel="noopener"

View file

@ -37,6 +37,7 @@ export default function UserAuthForm() {
avatar: '' avatar: ''
} }
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const pathname = usePathname()!; const pathname = usePathname()!;
@ -101,6 +102,7 @@ export default function UserAuthForm() {
<form <form
className="flex w-52 flex-col justify-center space-y-4 sm:w-72" className="flex w-52 flex-col justify-center space-y-4 sm:w-72"
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
method="POST"
> >
{!isSignIn && ( {!isSignIn && (
<> <>

View file

@ -90,13 +90,12 @@ function NavItem({
onClick?: () => void; onClick?: () => void;
}) { }) {
const segment = useSelectedLayoutSegment(); const segment = useSelectedLayoutSegment();
const pathname = segment?.split('/').pop() || '';
const isHttp = item.slug.includes('http'); const isHttp = item.slug.includes('http');
const isActive = pathname === item.slug || (item.slug === '' && !segment); const pathname = item.slug.split('/').pop();
const isActive = segment === pathname || (segment === null && pathname === 'dashboard');
return ( return (
<AppLink <AppLink
aria-disabled={item.disabled} href={isHttp ? item.slug : `/${item.slug}`}
href={isHttp ? item.slug : `dashboard/${item.slug}`}
target={isHttp ? '_blank' : undefined} target={isHttp ? '_blank' : undefined}
rel={isHttp ? 'noopener noreferrer' : undefined} rel={isHttp ? 'noopener noreferrer' : undefined}
className={cn( className={cn(