added somes ISR & loading stuff

This commit is contained in:
Théo 2023-03-06 17:51:51 +01:00
parent e96a44608b
commit 4ca1c599fd
14 changed files with 158 additions and 107 deletions

View file

@ -1,22 +1,4 @@
import { getScores } from '@/lib/leaderboard'; import Leaderboard from '@/ui/Leaderboard';
import { cn } from '@/lib/utils';
import Avatar from '@/ui/Avatar';
import Select from '@/ui/Select';
// TODO: Generate this later
const scoreColors = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
// TODO: Generate this later
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 const metadata = { export const metadata = {
title: 'Tableau des scores - Peer-at Code', title: 'Tableau des scores - Peer-at Code',
@ -24,49 +6,5 @@ export const metadata = {
}; };
export default async function Page() { export default async function Page() {
// TODO: CSR fetch data for leaderboard (useSWR) to make it more reactive return <Leaderboard />;
const data = await getScores();
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">
<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>
</div>
<Select className="w-28" options={options} />
</header>
<main className="flex flex-col justify-between space-x-0 space-y-4">
{data.map((score, key) => (
<div key={key} className="flex flex-col space-y-2">
<div 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} />
<span className="text-lg">{score.pseudo}</span>
<span className="text-sm text-muted">{score.group}</span>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex flex-col">
<span className="text-sm font-semibold">Puzzles</span>
<span className="text-lg text-muted">{score.completions}</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold">Score</span>
<span className="text-lg text-muted">{score.score}</span>
</div>
</div>
</div>
</div>
))}
</main>
</section>
</div>
</div>
);
} }

View file

@ -23,8 +23,10 @@ export default async function Page({ params }: { params: { id: number } }) {
return <Puzzle puzzle={puzzle} />; return <Puzzle puzzle={puzzle} />;
} }
// export const dynamicParams = true;
// export async function generateStaticParams() { // export async function generateStaticParams() {
// const { puzzles } = await getPuzzles(); // const { puzzles } = await getPuzzles();
// // every id is a number, but we need to return a string // // every id is a number, but we need to return a string
// return puzzles.map((puzzle) => ({ id: puzzle.id.toString() })); // return puzzles.flatMap((puzzle) => ({ id: puzzle.id.toString() }));
// } // }

View file

@ -1,4 +1,3 @@
import { getPuzzles } from '@/lib/puzzles';
import Puzzles from '@/ui/Puzzles'; import Puzzles from '@/ui/Puzzles';
export const metadata = { export const metadata = {
@ -6,10 +5,9 @@ export const metadata = {
}; };
export default async function Page() { export default async function Page() {
const data = await getPuzzles();
return ( return (
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<Puzzles data={data} /> <Puzzles />
</div> </div>
); );
} }

View file

@ -0,0 +1,6 @@
import useSWR from 'swr';
import { getScores } from '../leaderboard';
export function useLeaderboard() {
return useSWR('leaderboard', () => getScores());
}

View file

@ -17,8 +17,6 @@ export const getChapters = async (): Promise<Chapter[]> => {
chapters = chapters.filter((chapter: Chapter) => chapter.id !== 0); chapters = chapters.filter((chapter: Chapter) => chapter.id !== 0);
console.log(chapters);
return chapters as Chapter[]; return chapters as Chapter[];
}; };

View file

@ -20,8 +20,6 @@ export async function middleware(req: NextRequest) {
else if (req.nextUrl.pathname.includes('sign') && token) else if (req.nextUrl.pathname.includes('sign') && token)
return NextResponse.redirect(getURL('/dashboard')); return NextResponse.redirect(getURL('/dashboard'));
res.headers.set('Authorization', `Bearer ${token}`);
return res; return res;
} }

View file

@ -5,6 +5,5 @@
@layer components { @layer components {
.console { .console {
@apply relative top-0.5 inline-block; @apply relative top-0.5 inline-block;
/* make it hidden then visible every seconde */
} }
} }

View file

@ -29,8 +29,8 @@ module.exports = {
0: 'hsl(258deg 8% 100%)' 0: 'hsl(258deg 8% 100%)'
}, },
brand: { brand: {
DEFAULT: '#1c56cb', DEFAULT: '#5049ca',
accent: '#236bfe' accent: '#913fb6'
}, },
success: { success: {
DEFAULT: 'hsl(104deg 39% 59%)', DEFAULT: 'hsl(104deg 39% 59%)',
@ -55,11 +55,6 @@ module.exports = {
highlight: { highlight: {
primary: 'hsl(258deg 15% 17%)', primary: 'hsl(258deg 15% 17%)',
secondary: 'hsl(258deg 10% 46%)' secondary: 'hsl(258deg 10% 46%)'
},
product: {
ignite: 'hsl(8deg 89% 57%)',
pipe: 'hsl(214deg 100% 58%)',
channels: 'hsl(46deg 74% 51%)'
} }
}, },
backgroundColor: { backgroundColor: {

View file

@ -14,6 +14,7 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"downlevelIteration": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"

View file

@ -14,7 +14,7 @@ const Button = forwardRef<
{ {
'bg-highlight-primary hover:bg-highlight-primary/60': kind === 'default', 'bg-highlight-primary hover:bg-highlight-primary/60': kind === 'default',
'bg-error hover:bg-error/60': kind === 'danger', 'bg-error hover:bg-error/60': kind === 'danger',
'bg-brand hover:bg-brand/60': kind === 'brand' 'bg-gradient-to-tl from-brand to-brand-accent hover:bg-opacity-80': kind === 'brand'
}, },
className className
)} )}

View file

@ -14,7 +14,7 @@ const Input = forwardRef<
<Label label={label} description={description} required={props.required} className={className}> <Label label={label} description={description} required={props.required} className={className}>
<input <input
ref={ref} ref={ref}
className="w-full rounded-md border border-primary-600 bg-highlight-primary px-5 py-2.5 text-sm font-medium outline-none focus:border-brand focus:bg-primary-800 disabled:opacity-50" 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"
{...props} {...props}
/> />
</Label> </Label>

79
ui/Leaderboard.tsx Normal file
View file

@ -0,0 +1,79 @@
'use client';
import { useLeaderboard } from '@/lib/hooks/use-leaderboard';
import { cn } from '@/lib/utils';
import Avatar 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() {
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">
<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>
</div>
<Select className="w-28" options={options} />
</header>
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
{(!isLoading &&
data?.map((score, key) => (
<div key={key} className="flex flex-col space-y-2">
<div 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} />
<span className="text-lg">{score.pseudo}</span>
<span className="text-sm text-muted">{score.group}</span>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex flex-col">
<span className="text-sm font-semibold">Puzzles</span>
<span className="text-lg text-muted">{score.completions}</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold">Score</span>
<span className="text-lg text-muted">{score.score}</span>
</div>
</div>
</div>
</div>
))) ||
[...Array(20).keys()].map((i) => (
<span
key={i}
className="inline-block h-12 animate-pulse rounded-lg bg-primary-600"
style={{
animationDelay: `${i * 0.05}s`,
animationDuration: '1s'
}}
/>
))}
</main>
</section>
</div>
</div>
);
}

View file

@ -3,6 +3,7 @@
import type { Puzzle as PuzzleType } from '@/lib/puzzles'; import type { Puzzle as PuzzleType } from '@/lib/puzzles';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import cookies from 'js-cookie';
import Button from './Button'; import Button from './Button';
import Input from './Input'; import Input from './Input';
@ -10,6 +11,7 @@ import ToHTML from './ToHTML';
type PuzzleData = { type PuzzleData = {
answer: string; answer: string;
filename: string;
code_file: File[]; code_file: File[];
}; };
@ -26,6 +28,7 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
} = useForm<PuzzleData>({ } = useForm<PuzzleData>({
defaultValues: { defaultValues: {
answer: '', answer: '',
filename: '',
code_file: undefined code_file: undefined
} }
}); });
@ -34,11 +37,15 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
const formData = new FormData(); const formData = new FormData();
formData.append('answer', data.answer); formData.append('answer', data.answer);
formData.append('filename', data.code_file[0].name);
formData.append('code_file', data.code_file[0]); formData.append('code_file', data.code_file[0]);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzleResponse/${puzzle.id}`, { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzleResponse/${puzzle.id}`, {
method: 'POST', method: 'POST',
body: formData body: formData,
headers: {
Authorization: `Bearer ${cookies.get('token')}}`
}
}); });
if (res.ok) { if (res.ok) {

View file

@ -1,38 +1,68 @@
'use client'; 'use client';
import { Chapter, Puzzle } from '@/lib/puzzles'; import { usePuzzles } from '@/lib/hooks/use-puzzles';
import AppLink from './AppLink'; import AppLink from './AppLink';
import Icon from './Icon'; import Icon from './Icon';
export default function Puzzles({ data }: { data: { chapters: Chapter[]; puzzles: Puzzle[] } }) { export default function Puzzles() {
// const { data, isLoading } = usePuzzles(); const { data, isLoading } = usePuzzles();
return ( return (
<> <>
{data?.chapters?.map((chapter) => ( {(!isLoading &&
<div key={chapter.id} className="flex flex-col space-y-4"> data?.chapters?.map((chapter) => (
<div className="flex items-center justify-between"> <div key={chapter.id} className="flex flex-col space-y-4">
<h3 className="text-2xl font-semibold"> <div className="flex items-center justify-between">
Chapitre {chapter.id} - {chapter.name} <h3 className="text-xl font-semibold sm:text-2xl">
</h3> Chapitre {chapter.id} - {chapter.name}
<div className="h-1 w-1/4 rounded-lg bg-gray-200"> </h3>
<div className="h-1 w-1/2 rounded-lg bg-brand" /> <div className="h-1 w-1/4 rounded-lg bg-gray-200">
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
</div>
</div> </div>
<ul className="flex flex-col space-y-4">
{data?.puzzles.map((puzzle) => (
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}>
<li className="group flex items-center justify-between rounded-md bg-primary-700 p-4 font-code hover:bg-primary-600">
<span className="text-base font-semibold">{puzzle.name}</span>
<Icon
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
name="arrow-right-line"
/>
</li>
</AppLink>
))}
</ul>
</div> </div>
<ul className="flex flex-col space-y-4"> ))) || (
{data?.puzzles.map((puzzle) => ( <div className="flex flex-col space-y-6">
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}> {[...Array(3).keys()].map((i) => (
<li className="group flex justify-between rounded-md bg-primary-700 p-4 font-code hover:bg-primary-600"> <div key={i} className="flex flex-col space-y-4">
<span className="font-semibold">{puzzle.name}</span> <div className="flex items-center justify-between">
<Icon <span className="inline-block h-8 w-1/2 rounded-lg bg-primary-600" />
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0" <span
name="arrow-right-line" className="inline-block h-1 w-1/4 animate-pulse rounded-lg bg-primary-600"
style={{
animationDelay: `${i * 0.05}s`,
animationDuration: '1s'
}}
/>
</div>
<ul className="flex flex-col space-y-4">
{[...Array(7).keys()].map((j) => (
<span
key={j}
className="inline-block h-14 animate-pulse rounded-lg bg-primary-600"
style={{
animationDelay: `${j * 0.05}s`,
animationDuration: '1s'
}}
/> />
</li> ))}
</AppLink> </ul>
))} </div>
</ul> ))}
</div> </div>
))} )}
</> </>
); );
} }