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">
<header className="flex flex-col">
<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
</p>
</header>

View file

@ -36,13 +36,17 @@ export default function Page() {
<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"></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 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"></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>
</div>
{/* TODO fix ça c'est pas responsive */}

View file

@ -25,7 +25,11 @@ export default async function Page({ params }: { params: { id: number } }) {
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) {
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 { type Group } from './groups';
export const getPlayer = async ({
token,
@ -46,8 +47,3 @@ export type Badge = {
level: number;
logo?: string;
};
export type Group = {
name: string;
chapter?: number;
};

View file

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

View file

@ -11,10 +11,34 @@ export async function middleware(req: NextRequest) {
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'));
else if (req.nextUrl.pathname.includes('sign') && token)
}
if (token && req.nextUrl.pathname.includes('sign')) {
return NextResponse.redirect(getURL('/dashboard'));
}
return res;
}

View file

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

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">
<span className="text-lg">{score.pseudo}</span>
<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>
</div>
</div>

View file

@ -1,9 +1,9 @@
'use client';
import type { Puzzle as PuzzleType } from '@/lib/puzzles';
import cookies from 'js-cookie';
import { notFound } from 'next/navigation';
import { useForm } from 'react-hook-form';
import cookies from 'js-cookie';
import Button from './Button';
import Input from './Input';
@ -36,9 +36,14 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
async function onSubmit(data: PuzzleData) {
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('filename', data.code_file[0].name);
formData.append('code_file', data.code_file[0]);
// formData.append('filename', data.code_file[0].name);
// formData.append('code_file', data.code_file[0]);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzleResponse/${puzzle.id}`, {
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 flex-col space-y-2">
<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 className="flex h-screen overflow-y-auto">
<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">
<Input
className="w-full sm:w-1/3"
className="w-full"
label="Réponse"
type="text"
placeholder="12"
required
{...register('answer')}
/>
<Input
{/* <Input
className="h-16 w-full sm:w-1/3"
label="Code"
type="file"
required
accept=".py,.js,.ts,.java,.rust,.c"
accept=".py,.js,.ts,.java,.rs,.c"
{...register('code_file')}
/>
/> */}
</div>
<Button kind="brand" className="mt-6" type="submit">
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) {
setError('email', {
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"}
</Button>
<div className="flex flex-col text-center">
{!isSignIn && (
{/* {!isSignIn && (
<p className="flex flex-col items-center text-sm text-muted">
En cliquant sur continuer, vous acceptez les{' '}
<AppLink className="text-white underline" href="/privacy-policy" target="_blank">
@ -142,7 +142,7 @@ export default function UserAuthForm() {
</AppLink>
.
</p>
)}
)} */}
<p className="flex flex-col items-center text-sm text-muted">
{isSignIn ? "Vous n'avez pas de compte?" : 'Vous possédez un compte?'}{' '}
<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() {
cookies.remove('token');
router.refresh();
router.replace('/');
}
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 items-center">
<button onClick={toggle} className="block sm:hidden">