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';
import { useContext } from 'react';
import { UserContext } from '@/context/user';
import Card from '@/ui/Card';
import { useContext } from 'react';
export default function Page() {
const { data: me, isLoading } = useContext(UserContext);
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">
<section className="w-full flex-col space-y-4">
<header>
<h3 className="text-xl font-semibold">Tableau de bord</h3>
<p className="text-muted">Ceci est la page d&apos;accueil du dashboard</p>
<h1 className="text-xl font-semibold">Tableau de bord</h1>
<p className="text-highlight-secondary">Ceci est la page d&apos;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">
<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
isLoading={isLoading}
icon="pie-chart-line"
title="Puzzles résolus"
data={me?.completions || 0}
data={me?.completions ?? 0}
link="/dashboard/puzzles"
/>
<Card
isLoading={isLoading}
icon="award-line"
title="Badges obtenus"
data={me?.badges?.length || 'Aucun'}
data={me?.badges?.length ?? 'Aucun'}
link="/dashboard/badges"
/>
<Card
isLoading={isLoading}
icon="bar-chart-line"
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>
</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(() => {
router.push('/');
}, []);
}, [router]);
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[]}
*/
export const navItems: NavItem[] = [
// {
// name: 'Dashboard',
// slug: '',
// icon: 'dashboard-line',
// disabled: false
// },
{
name: 'Dashboard',
slug: 'dashboard',
icon: 'dashboard-line',
disabled: false
},
{
name: 'Classement',
slug: 'leaderboard',
slug: 'dashboard/leaderboard',
icon: 'line-chart-line',
disabled: false
},
{
name: 'Puzzles',
slug: 'puzzles',
slug: 'dashboard/puzzles',
icon: 'code-s-slash-line',
disabled: false
},
{
name: 'Badges',
slug: 'badges',
slug: 'dashboard/badges',
icon: 'award-fill',
disabled: false
},
{
name: 'Paramètres',
slug: 'settings',
slug: 'dashboard/settings',
icon: 'equalizer-line',
disabled: false
}

View file

@ -38,6 +38,7 @@ export type Player = {
tries: number;
completions: number;
rank: number;
completionsList: Completion[];
badges: Badge[] | null;
};
@ -46,3 +47,9 @@ export type Badge = {
level: number;
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';
export default function Card({
isLoading,
icon,
title,
data
data,
link
}: {
isLoading: boolean;
icon: string;
title: string;
data: any;
link?: string;
}) {
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" />
<span className="h-[18px] w-32 animate-pulse rounded bg-highlight-primary" />
<span className="h-[18px] w-24 animate-pulse rounded bg-highlight-primary" />
</div>
</div>
);
@ -25,10 +30,20 @@ export default function Card({
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">{data}</h3>
<div className="flex w-full items-center justify-between">
<div className="flex-col">
<h2 className="text-xl font-semibold">{data}</h2>
<p className="text-muted">{title}</p>
</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>
);
}

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';
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 Podium from '@/ui/events/podium/Podium';
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'];
export default function Leaderboard() {
export default function Leaderboard({ token }: { token: string }) {
// TODO CHANGER CECI
const CHAPITRE_EVENT = 1;
const subscription: SWRSubscription<string, ScoreEvent, Error> = (key, { next }) => {
const socket = new WebSocket(key);
// const subscription: SWRSubscription<string, ScoreEvent, Error> = (key, { next }) => {
// const socket = new WebSocket(key);
socket.addEventListener('message', (event) => {
next(null, JSON.parse(event.data));
});
// socket.addEventListener('message', (event) => {
// next(null, JSON.parse(event.data));
// });
socket.addEventListener('error', (event) => {
console.error(event);
});
// socket.addEventListener('error', (event) => {
// console.error(event);
// });
return () => socket.close();
};
// return () => socket.close();
// };
const { data } = useSWRSubscription(
`wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${CHAPITRE_EVENT}`,
subscription
);
// const { data } = useSWRSubscription(
// `wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${CHAPITRE_EVENT}`,
// subscription
// );
const { data, isLoading } = useLeaderboardEvent({ token: token, id: CHAPITRE_EVENT });
const scores = [data?.groups]
.flat()
@ -60,8 +63,8 @@ export default function Leaderboard() {
const tries = group.players.reduce((a, b) => a + b.tries, 0) || 0;
return (
<motion.li
layout
initial={{ opacity: 0, y: -10 }}
// @ts-ignore TODO Je sais pas c'est quoi cette merde
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.5 }}
@ -69,13 +72,18 @@ export default function Leaderboard() {
className="flex justify-between space-x-2"
>
<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}
</span>
<div className="flex items-center space-x-2">
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
<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
.map((player) => player.pseudo || 'Anonyme')
@ -89,15 +97,15 @@ export default function Leaderboard() {
<div className="flex items-center space-x-4">
{/* <div className="flex flex-col">
<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 className="flex flex-col">
<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 className="flex flex-col">
<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)}
</span>
</div>

View file

@ -257,14 +257,14 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
{puzzle.tags
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
.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}
</span>
))}
</div>
)}
<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"
/>
</div>
@ -292,7 +292,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
</div>
)}
<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"
/>
</div>

View file

@ -13,7 +13,7 @@ export default function ToHTML({ data, className }: { data: string; className?:
a: ({ node, ...props }) => (
<a
{...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
target="_blank"
rel="noopener"

View file

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

View file

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