Misc change

This commit is contained in:
Théo 2023-04-11 11:23:50 +02:00
parent 21ebeb8a6d
commit 82d7291068
16 changed files with 710 additions and 243 deletions

View file

@ -12,7 +12,7 @@ export default function Page() {
<section className="flex flex-col space-y-4"> <section className="flex flex-col space-y-4">
<header className="flex flex-col"> <header className="flex flex-col">
<h3 className="text-xl font-semibold">Mes badges</h3> <h3 className="text-xl font-semibold">Mes badges</h3>
<p className="hidden text-muted sm:block"> <p className="text-muted">
Vos badges sont affichés ici, vous pouvez les partager avec vos amis Vos badges sont affichés ici, vous pouvez les partager avec vos amis
</p> </p>
</header> </header>

View file

@ -36,13 +36,17 @@ export default function Page() {
<header> <header>
<h3 className="text-xl font-semibold">Guides</h3> <h3 className="text-xl font-semibold">Guides</h3>
</header> </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"></main> <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>
<section className="flex h-full w-full flex-col space-y-4"> <section className="flex h-full w-full flex-col space-y-4">
<header> <header>
<h3 className="text-xl font-semibold">Historiques</h3> <h3 className="text-xl font-semibold">Historiques</h3>
</header> </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"></main> <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>
</div> </div>
{/* TODO fix ça c'est pas responsive */} {/* TODO fix ça c'est pas responsive */}

View file

@ -25,7 +25,11 @@ export default async function Page({ params }: { params: { id: number } }) {
notFound(); notFound();
} }
const puzzle = await getPuzzle({ token, id }); const puzzle = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzle/${id}`, {
headers: {
Authorization: `Bearer ${token}`
}
}).then((res) => res.json());
if (!puzzle) { if (!puzzle) {
notFound(); notFound();

27
lib/groups.ts Normal file
View file

@ -0,0 +1,27 @@
import fetcher from './fetcher';
export const getGroups = async ({ token }: { token: string }): Promise<Group[]> => {
const { data, status } = await fetcher.get(`/groups`, {
headers: {
Authorization: `Bearer ${token}`
}
});
const groups = data;
if (status !== 200) {
throw new Error('Failed to fetch groups');
}
if (!groups) {
return [] as Group[];
}
return groups as Group[];
};
export type Group = {
id: number;
name: string;
chapter?: number;
};

6
lib/hooks/use-groups.ts Normal file
View file

@ -0,0 +1,6 @@
import useSWR from 'swr';
import { getGroups } from '../groups';
export function useGroups({ token }: { token: string }) {
return useSWR('groups', () => getGroups({ token }));
}

View file

@ -1,4 +1,5 @@
import fetcher from './fetcher'; import fetcher from './fetcher';
import { type Group } from './groups';
export const getPlayer = async ({ export const getPlayer = async ({
token, token,
@ -46,8 +47,3 @@ export type Badge = {
level: number; level: number;
logo?: string; logo?: string;
}; };
export type Group = {
name: string;
chapter?: number;
};

View file

@ -90,8 +90,8 @@ export type Chapter = {
id: number; id: number;
name: string; name: string;
puzzles: Puzzle[]; puzzles: Puzzle[];
startDay?: string; startDate?: string;
endDay?: string; endDate?: string;
}; };
export type Tag = { export type Tag = {

View file

@ -11,10 +11,34 @@ export async function middleware(req: NextRequest) {
const token = req.cookies.get('token')?.value; const token = req.cookies.get('token')?.value;
if (req.nextUrl.pathname.includes('dashboard') && !token) if (token) {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/player/`, {
headers: {
Authorization: `Bearer ${token}`
},
cache: 'no-cache',
next: {
revalidate: 60
}
});
if (response.status !== 200) {
res.cookies.set('token', '', {
path: '/',
expires: new Date(0)
});
NextResponse.redirect(getURL('/sign-in'));
}
}
if (!token && req.nextUrl.pathname.includes('dashboard')) {
return NextResponse.redirect(getURL('/sign-in')); return NextResponse.redirect(getURL('/sign-in'));
else if (req.nextUrl.pathname.includes('sign') && token) }
if (token && req.nextUrl.pathname.includes('sign')) {
return NextResponse.redirect(getURL('/dashboard')); return NextResponse.redirect(getURL('/dashboard'));
}
return res; return res;
} }

View file

@ -20,16 +20,20 @@
}, },
"homepage": "https://github.com/Peer-at-Code/peer-at-code#readme", "homepage": "https://github.com/Peer-at-Code/peer-at-code#readme",
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-popover": "^1.0.5", "@radix-ui/react-popover": "^1.0.5",
"axios": "^1.3.4", "axios": "^1.3.4",
"boring-avatars": "^1.7.0", "boring-avatars": "^1.7.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"framer-motion": "^10.11.2",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"next": "13.2.3", "next": "13.2.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.1", "react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"remixicon": "^2.5.0", "remixicon": "^2.5.0",
"swr": "^2.0.3", "swr": "^2.0.3",
"tailwind-merge": "^1.9.0", "tailwind-merge": "^1.9.0",

768
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,9 @@ const AppLink = forwardRef<HTMLAnchorElement, Parameters<typeof Link>[0]>((props
if (props.target === '_blank') { if (props.target === '_blank') {
return <a ref={ref} {...props} href={props.href.toString()} />; return <a ref={ref} {...props} href={props.href.toString()} />;
} }
if (props['aria-disabled']) {
return <span ref={ref} {...props} />;
}
return <Link ref={ref} {...props} href={props.href} />; return <Link ref={ref} {...props} href={props.href} />;
}); });

View file

@ -74,7 +74,10 @@ export default function Leaderboard({ token }: { token: string }) {
<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">{score.pseudo}</span> <span className="text-lg">{score.pseudo}</span>
<span className="text-sm text-muted"> <span className="text-sm text-muted">
{score.groups?.map((g) => g.name).join(', ')} {score.groups
?.map((g) => g.name)
.sort((a, b) => a.localeCompare(b))
.join(', ')}
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,9 +1,9 @@
'use client'; 'use client';
import type { Puzzle as PuzzleType } from '@/lib/puzzles'; import type { Puzzle as PuzzleType } from '@/lib/puzzles';
import cookies from 'js-cookie';
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';
@ -36,9 +36,14 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
async function onSubmit(data: PuzzleData) { async function onSubmit(data: PuzzleData) {
const formData = new FormData(); const formData = new FormData();
// if (data.code_file[0].size > 16 * 1024 * 1024) {
// alert('Fichier trop volumineux');
// return;
// }
formData.append('answer', data.answer); formData.append('answer', data.answer);
formData.append('filename', data.code_file[0].name); // 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',
@ -57,7 +62,7 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
<div className="flex h-full w-full flex-col justify-between space-y-4"> <div className="flex h-full w-full flex-col justify-between space-y-4">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<h2 className="text-4xl font-bold">{puzzle.name}</h2> <h2 className="text-4xl font-bold">{puzzle.name}</h2>
<p className="text-sm text-muted">Chapitre</p> {/* <p className="text-sm text-muted">Chapitre</p> */}
</div> </div>
<div className="flex h-screen overflow-y-auto"> <div className="flex h-screen overflow-y-auto">
<ToHTML className="font-code" data={puzzle.content} /> <ToHTML className="font-code" data={puzzle.content} />
@ -69,21 +74,21 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
> >
<div className="flex flex-col space-x-0 sm:flex-row sm:space-x-6"> <div className="flex flex-col space-x-0 sm:flex-row sm:space-x-6">
<Input <Input
className="w-full sm:w-1/3" className="w-full"
label="Réponse" label="Réponse"
type="text" type="text"
placeholder="12" placeholder="12"
required required
{...register('answer')} {...register('answer')}
/> />
<Input {/* <Input
className="h-16 w-full sm:w-1/3" className="h-16 w-full sm:w-1/3"
label="Code" label="Code"
type="file" type="file"
required required
accept=".py,.js,.ts,.java,.rust,.c" accept=".py,.js,.ts,.java,.rs,.c"
{...register('code_file')} {...register('code_file')}
/> /> */}
</div> </div>
<Button kind="brand" className="mt-6" type="submit"> <Button kind="brand" className="mt-6" type="submit">
Envoyer Envoyer

55
ui/Timer.tsx Normal file
View file

@ -0,0 +1,55 @@
import clsx from 'clsx';
import { useEffect, useReducer } from 'react';
type State = {
hours: number;
minutes: number;
seconds: number;
};
type Action = {
type: string;
payload: Partial<State>;
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_TIME_REMAINING':
return { ...state, ...action.payload };
default:
return state;
}
}
export function Timer({ targetDate, className }: { targetDate: Date; className?: string }) {
const [timeRemaining, dispatch] = useReducer(reducer, {
hours: 0,
minutes: 0,
seconds: 0
});
targetDate = new Date(targetDate);
useEffect(() => {
const intervalId = setInterval(() => {
const timeDifference = targetDate.getTime() - Date.now();
const hours = Math.floor(timeDifference / (1000 * 60 * 60));
const minutes = Math.floor((timeDifference / (1000 * 60)) % 60);
const seconds = Math.floor((timeDifference / 1000) % 60);
dispatch({
type: 'SET_TIME_REMAINING',
payload: { hours, minutes, seconds }
});
}, 1000);
return () => clearInterval(intervalId);
}, [targetDate]);
return (
<span className={clsx('', className)}>
{`${timeRemaining.hours.toString().padStart(2, '0')}:${timeRemaining.minutes
.toString()
.padStart(2, '0')}:${timeRemaining.seconds.toString().padStart(2, '0')}`}
</span>
);
}

View file

@ -64,7 +64,7 @@ export default function UserAuthForm() {
if (!email_valid) { if (!email_valid) {
setError('email', { setError('email', {
type: 'manual', type: 'manual',
message: 'Email déjà utilisé' message: 'Adresse e-mail indisponible'
}); });
} }
} }
@ -134,7 +134,7 @@ export default function UserAuthForm() {
{isSignIn ? 'Se connecter' : "S'inscrire"} {isSignIn ? 'Se connecter' : "S'inscrire"}
</Button> </Button>
<div className="flex flex-col text-center"> <div className="flex flex-col text-center">
{!isSignIn && ( {/* {!isSignIn && (
<p className="flex flex-col items-center text-sm text-muted"> <p className="flex flex-col items-center text-sm text-muted">
En cliquant sur continuer, vous acceptez les{' '} En cliquant sur continuer, vous acceptez les{' '}
<AppLink className="text-white underline" href="/privacy-policy" target="_blank"> <AppLink className="text-white underline" href="/privacy-policy" target="_blank">
@ -142,7 +142,7 @@ export default function UserAuthForm() {
</AppLink> </AppLink>
. .
</p> </p>
)} )} */}
<p className="flex flex-col items-center text-sm text-muted"> <p className="flex flex-col items-center text-sm text-muted">
{isSignIn ? "Vous n'avez pas de compte?" : 'Vous possédez un compte?'}{' '} {isSignIn ? "Vous n'avez pas de compte?" : 'Vous possédez un compte?'}{' '}
<AppLink className="text-brand underline" href={isSignIn ? '/sign-up' : '/sign-in'}> <AppLink className="text-brand underline" href={isSignIn ? '/sign-up' : '/sign-in'}>

View file

@ -24,11 +24,11 @@ export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: (
async function handleLogout() { async function handleLogout() {
cookies.remove('token'); cookies.remove('token');
router.refresh(); router.replace('/');
} }
return ( 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="z-50 flex w-full flex-row items-center justify-between border-b border-solid border-highlight-primary bg-secondary px-8 py-4">
<div className="flex flex-row items-center space-x-2 sm:space-x-0"> <div className="flex flex-row items-center space-x-2 sm:space-x-0">
<div className="flex items-center"> <div className="flex items-center">
<button onClick={toggle} className="block sm:hidden"> <button onClick={toggle} className="block sm:hidden">