added somes ISR & loading stuff
This commit is contained in:
parent
e96a44608b
commit
4ca1c599fd
14 changed files with 158 additions and 107 deletions
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }));
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
6
lib/hooks/use-leaderboard.ts
Normal file
6
lib/hooks/use-leaderboard.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { getScores } from '../leaderboard';
|
||||||
|
|
||||||
|
export function useLeaderboard() {
|
||||||
|
return useSWR('leaderboard', () => getScores());
|
||||||
|
}
|
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
|
|
|
@ -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
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
79
ui/Leaderboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -1,28 +1,29 @@
|
||||||
'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 &&
|
||||||
|
data?.chapters?.map((chapter) => (
|
||||||
<div key={chapter.id} className="flex flex-col space-y-4">
|
<div key={chapter.id} className="flex flex-col space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-2xl font-semibold">
|
<h3 className="text-xl font-semibold sm:text-2xl">
|
||||||
Chapitre {chapter.id} - {chapter.name}
|
Chapitre {chapter.id} - {chapter.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-1 w-1/4 rounded-lg bg-gray-200">
|
<div className="h-1 w-1/4 rounded-lg bg-gray-200">
|
||||||
<div className="h-1 w-1/2 rounded-lg bg-brand" />
|
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="flex flex-col space-y-4">
|
<ul className="flex flex-col space-y-4">
|
||||||
{data?.puzzles.map((puzzle) => (
|
{data?.puzzles.map((puzzle) => (
|
||||||
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}>
|
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}>
|
||||||
<li className="group flex justify-between rounded-md bg-primary-700 p-4 font-code hover:bg-primary-600">
|
<li className="group flex items-center justify-between rounded-md bg-primary-700 p-4 font-code hover:bg-primary-600">
|
||||||
<span className="font-semibold">{puzzle.name}</span>
|
<span className="text-base font-semibold">{puzzle.name}</span>
|
||||||
<Icon
|
<Icon
|
||||||
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
|
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
|
||||||
name="arrow-right-line"
|
name="arrow-right-line"
|
||||||
|
@ -32,7 +33,36 @@ export default function Puzzles({ data }: { data: { chapters: Chapter[]; puzzles
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
))) || (
|
||||||
|
<div className="flex flex-col space-y-6">
|
||||||
|
{[...Array(3).keys()].map((i) => (
|
||||||
|
<div key={i} className="flex flex-col space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="inline-block h-8 w-1/2 rounded-lg bg-primary-600" />
|
||||||
|
<span
|
||||||
|
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'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue