Big update & refactor & metadata
This commit is contained in:
parent
0773cba2c8
commit
cc1fb080b5
21 changed files with 235 additions and 212 deletions
|
@ -1,40 +1,38 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { UserContext } from '@/context/user';
|
|
||||||
import Badge from '@/ui/Badge';
|
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { UserContext } from '@/context/user';
|
||||||
|
|
||||||
|
import Badge from '@/ui/Badge';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { data: me } = useContext(UserContext);
|
const { data: me } = useContext(UserContext);
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col space-y-4">
|
<section className="flex h-full w-full flex-col space-y-4">
|
||||||
<div className="w-full">
|
<header className="flex flex-col">
|
||||||
<section className="flex flex-col space-y-4">
|
<h1 className="text-xl font-semibold">Mes badges</h1>
|
||||||
<header className="flex flex-col">
|
<p className="text-muted">
|
||||||
<h3 className="text-xl font-semibold">Mes badges</h3>
|
Vos badges sont affichés ici, vous pouvez les partager avec vos amis
|
||||||
<p className="text-muted">
|
</p>
|
||||||
Vos badges sont affichés ici, vous pouvez les partager avec vos amis
|
</header>
|
||||||
</p>
|
<main className="flex flex-col justify-between space-x-0 space-y-4">
|
||||||
</header>
|
<div className="flex space-x-2">
|
||||||
<main className="flex flex-col justify-between space-x-0 space-y-4">
|
{me?.badges ? (
|
||||||
<div className="flex space-x-2">
|
me?.badges.map((badge, i) => (
|
||||||
{me?.badges ? (
|
<Badge
|
||||||
me?.badges.map((badge, i) => (
|
key={i}
|
||||||
<Badge
|
name={badge.name}
|
||||||
key={i}
|
src={badge.logo || '/assets/badges/java.png'}
|
||||||
name={badge.name}
|
alt={badge.name}
|
||||||
src={badge.logo || '/assets/badges/java.png'}
|
level={badge.level}
|
||||||
alt={badge.name}
|
/>
|
||||||
level={badge.level}
|
))
|
||||||
/>
|
) : (
|
||||||
))
|
<p className="text-muted">Aucun badge</p>
|
||||||
) : (
|
)}
|
||||||
<p className="text-muted">Aucun badge</p>
|
</div>
|
||||||
)}
|
</main>
|
||||||
</div>
|
</section>
|
||||||
</main>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="m-auto flex h-screen flex-col items-center justify-center space-y-6">
|
<div className="m-auto flex h-screen flex-col items-center justify-center space-y-6">
|
||||||
<h2 className="text-6xl">Oh non! Un François 404</h2>
|
<h2 className="text-6xl">Oh non! Un François 404</h2>
|
||||||
<Image src={error404} alt="François 404" width={1000} height={1000} />
|
<Image priority src={error404} alt="François 404" width={1000} height={1000} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { getPuzzle } from '@/lib/puzzles';
|
import { getPuzzle } from '@/lib/puzzles';
|
||||||
import Puzzle from '@/ui/Puzzle';
|
import Puzzle from '@/ui/Puzzle';
|
||||||
|
import SWRFallback from '@/ui/SWRFallback';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: { id: number } }): Promise<Metadata> {
|
export async function generateMetadata({ params }: { params: { id: number } }): Promise<Metadata> {
|
||||||
|
@ -17,8 +18,7 @@ export async function generateMetadata({ params }: { params: { id: number } }):
|
||||||
return { title: `${puzzle.name} - Peer-at Code` };
|
return { title: `${puzzle.name} - Peer-at Code` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params }: { params: { id: number } }) {
|
export default async function Page({ params: { id } }: { params: { id: number } }) {
|
||||||
const { id } = params;
|
|
||||||
const token = cookies().get('token')?.value;
|
const token = cookies().get('token')?.value;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -35,13 +35,9 @@ export default async function Page({ params }: { params: { id: number } }) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Puzzle puzzle={puzzle} />;
|
return (
|
||||||
|
<SWRFallback fallback={{ [`puzzles/${puzzle.id}`]: puzzle }}>
|
||||||
|
<Puzzle token={token} id={id} />
|
||||||
|
</SWRFallback>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const dynamicParams = true;
|
|
||||||
|
|
||||||
// export async function generateStaticParams() {
|
|
||||||
// const { puzzles } = await getPuzzles();
|
|
||||||
// // every id is a number, but we need to return a string
|
|
||||||
// return puzzles.flatMap((puzzle) => ({ id: puzzle.id.toString() }));
|
|
||||||
// }
|
|
||||||
|
|
|
@ -12,7 +12,9 @@ export default async function Page() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
|
{/* <SWRFallback fallback={{ ['puzzles']: chapters }}> */}
|
||||||
<Puzzles token={token!} />
|
<Puzzles token={token!} />
|
||||||
|
{/* </SWRFallback> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-full">
|
|
||||||
<div className="m-auto">
|
|
||||||
<h1 className="text-center text-4xl font-bold">
|
|
||||||
Amuse toi avec <span className="rounded-md bg-white p-1 text-black ">Next.js</span> et{' '}
|
|
||||||
<span className="text-blue-500">Tailwindcss</span> !
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import 'remixicon/fonts/remixicon.css';
|
import 'remixicon/fonts/remixicon.css';
|
||||||
|
|
||||||
|
import { type Metadata } from 'next';
|
||||||
import { Fira_Code } from 'next/font/google';
|
import { Fira_Code } from 'next/font/google';
|
||||||
import localFont from 'next/font/local';
|
import localFont from 'next/font/local';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn, getURL } from '@/lib/utils';
|
||||||
import DefaultTags from '@/ui/DefaultTags';
|
|
||||||
|
|
||||||
const sans = localFont({
|
const sans = localFont({
|
||||||
variable: '--font-sans',
|
variable: '--font-sans',
|
||||||
|
@ -20,9 +20,59 @@ const code = Fira_Code({
|
||||||
weight: 'variable'
|
weight: 'variable'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Peer-at Code',
|
title: {
|
||||||
description: 'Peer-at Code a pour but de donner l’envie de coder et d’apprendre par le jeu!'
|
default: 'Peer-at Code',
|
||||||
|
template: `%s - Peer-at Code`
|
||||||
|
},
|
||||||
|
description: "Apprendre la programmation et la cybersécurité en s'amusant.",
|
||||||
|
// manifest: getURL('/favicon/site.webmanifest'),
|
||||||
|
openGraph: {
|
||||||
|
title: {
|
||||||
|
default: 'Peer-at Code',
|
||||||
|
template: `%s - Peer-at Code`
|
||||||
|
},
|
||||||
|
description: "Apprendre la programmation et la cybersécurité en s'amusant.",
|
||||||
|
url: getURL(),
|
||||||
|
siteName: 'Peer-at Code',
|
||||||
|
// images: getURL('/assets/social.jpg'),
|
||||||
|
type: 'website'
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: {
|
||||||
|
default: 'Peer-at Code',
|
||||||
|
template: `%s - Peer-at Code`
|
||||||
|
},
|
||||||
|
description: "Apprendre la programmation et la cybersécurité en s'amusant."
|
||||||
|
// images: getURL('/assets/social.jpg'),
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: getURL()
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{
|
||||||
|
url: getURL('/assets/icons/favicon-32x32.png'),
|
||||||
|
sizes: '32x32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: getURL('/assets/icons/favicon-16x16.png'),
|
||||||
|
sizes: '16x16'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shortcut: getURL('/favicon.ico'),
|
||||||
|
apple: getURL('/assets/icons/apple-touch-icon.png')
|
||||||
|
},
|
||||||
|
themeColor: '#110F15'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
|
@ -36,32 +86,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
code.variable
|
code.variable
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<head>
|
<head />
|
||||||
<DefaultTags />
|
|
||||||
|
|
||||||
{/* TODO: Use Metadata from 13.2 */}
|
|
||||||
|
|
||||||
{/* <title>Peer-at Code</title>
|
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="https://peer-at-code.be/" />
|
|
||||||
<meta property="og:title" content="Peer-at Code" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Peer-at Code a pour but de donner l’envie de coder et d’apprendre par le jeu!"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta property="og:image" content="/assets/social.jpg" />
|
|
||||||
|
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
|
||||||
<meta property="twitter:url" content="https://peet-at-code.be/" />
|
|
||||||
<meta property="twitter:title" content="Peer-at Code" />
|
|
||||||
<meta
|
|
||||||
property="twitter:description"
|
|
||||||
content="Peer-at Code a pour but de donner l’envie de coder et d’apprendre par le jeu!"
|
|
||||||
/>
|
|
||||||
<meta property="twitter:image" content="/assets/social.jpg" /> */}
|
|
||||||
</head>
|
|
||||||
<body className="relative min-h-screen">
|
<body className="relative min-h-screen">
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -6,22 +6,24 @@ export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex h-screen w-full">
|
<div className="flex h-screen w-full">
|
||||||
<div className="m-auto flex flex-col space-y-2">
|
<div className="m-auto flex flex-col space-y-2 p-2">
|
||||||
<h1 className="text-center text-6xl font-bold">Bienvenue sur</h1>
|
<h1 className="text-center text-6xl font-bold">Bienvenue sur</h1>
|
||||||
<span>
|
<span>
|
||||||
<Console text="Peer-at Code" className="text-6xl" />
|
<Console text="Peer-at Code" className="text-6xl" />
|
||||||
</span>
|
</span>
|
||||||
<AppLink href="/dashboard">Dashboard</AppLink>
|
<AppLink href="/dashboard/puzzles">Commencer</AppLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="item-center flex h-screen w-full px-2">
|
<div className="item-center flex h-screen w-full px-2">
|
||||||
<div className="m-auto flex flex-col justify-center md:flex-row">
|
<div className="m-auto flex flex-col justify-center md:flex-row">
|
||||||
<Image
|
<Image
|
||||||
|
title="Philipz 'Cipher Wolf' Barlow"
|
||||||
src="/assets/brand/peerat.png"
|
src="/assets/brand/peerat.png"
|
||||||
width={500}
|
width={500}
|
||||||
height={500}
|
height={500}
|
||||||
alt="Peer-at Code logo"
|
alt="Peer-at Code logo"
|
||||||
className="self-center"
|
className="self-center"
|
||||||
|
priority
|
||||||
/>
|
/>
|
||||||
<p className="self-center text-justify sm:w-3/6 md:w-2/6">
|
<p className="self-center text-justify sm:w-3/6 md:w-2/6">
|
||||||
BIP BZZ BIIIP, HIIP, HELIP, HELLO, je suis Philipz ‘Cipher Wolf’ Barlow, une
|
BIP BZZ BIIIP, HIIP, HELIP, HELLO, je suis Philipz ‘Cipher Wolf’ Barlow, une
|
||||||
|
|
|
@ -20,12 +20,12 @@ export type NavItem = {
|
||||||
* @type {NavItem[]}
|
* @type {NavItem[]}
|
||||||
*/
|
*/
|
||||||
export const navItems: NavItem[] = [
|
export const navItems: NavItem[] = [
|
||||||
{
|
// {
|
||||||
name: 'Dashboard',
|
// name: 'Dashboard',
|
||||||
slug: '',
|
// slug: '',
|
||||||
icon: 'dashboard-line',
|
// icon: 'dashboard-line',
|
||||||
disabled: false
|
// disabled: false
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: 'Classement',
|
name: 'Classement',
|
||||||
slug: 'leaderboard',
|
slug: 'leaderboard',
|
||||||
|
|
|
@ -83,7 +83,9 @@ export type Puzzle = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
scoreMax: number;
|
||||||
tags: Tag[] | null;
|
tags: Tag[] | null;
|
||||||
|
score?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Chapter = {
|
export type Chapter = {
|
||||||
|
|
|
@ -27,7 +27,12 @@ export async function middleware(req: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAuth && req.nextUrl.pathname.includes('sign')) {
|
if (isAuth && req.nextUrl.pathname.includes('sign')) {
|
||||||
return NextResponse.redirect(getURL('/dashboard'));
|
return NextResponse.redirect(getURL('/dashboard/puzzles'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO REMOVE
|
||||||
|
if (isAuth && req.nextUrl.pathname.match(/^\/dashboard\/?$/)) {
|
||||||
|
return NextResponse.redirect(getURL('/dashboard/puzzles'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -11,10 +11,7 @@
|
||||||
input:-webkit-autofill:focus,
|
input:-webkit-autofill:focus,
|
||||||
textarea:-webkit-autofill,
|
textarea:-webkit-autofill,
|
||||||
textarea:-webkit-autofill:hover,
|
textarea:-webkit-autofill:hover,
|
||||||
textarea:-webkit-autofill:focus,
|
textarea:-webkit-autofill:focus {
|
||||||
select:-webkit-autofill,
|
|
||||||
select:-webkit-autofill:hover,
|
|
||||||
select:-webkit-autofill:focus {
|
|
||||||
-webkit-box-shadow: 0 0 0px 1000px hsl(258deg 15% 17%) inset;
|
-webkit-box-shadow: 0 0 0px 1000px hsl(258deg 15% 17%) inset;
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
export default function DefaultTags() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<link href="/favicon.ico" rel="shortcut icon" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ import Icon from './Icon';
|
||||||
|
|
||||||
type DialogProps = {
|
type DialogProps = {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
tooltip: ReactNode;
|
tooltip?: ReactNode;
|
||||||
trigger: ReactNode;
|
trigger: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
|
|
|
@ -40,8 +40,8 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
return (
|
return (
|
||||||
<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 className="sticky flex items-center justify-between">
|
<header className="sticky flex items-center justify-between">
|
||||||
<div>
|
<div className="flex flex-col">
|
||||||
<h3 className="text-xl font-semibold">Tableau des scores</h3>
|
<h1 className="text-xl font-semibold">Tableau des scores</h1>
|
||||||
<p className="hidden text-muted sm:block">Suivez la progression des élèves en direct</p>
|
<p className="hidden text-muted sm:block">Suivez la progression des élèves en direct</p>
|
||||||
</div>
|
</div>
|
||||||
{(filteredData && (
|
{(filteredData && (
|
||||||
|
|
102
ui/Puzzle.tsx
102
ui/Puzzle.tsx
|
@ -1,14 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { Puzzle as PuzzleType } from '@/lib/puzzles';
|
|
||||||
import cookies from 'js-cookie';
|
import cookies from 'js-cookie';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
|
import { usePuzzle } from '@/lib/hooks/use-puzzles';
|
||||||
|
|
||||||
|
import AppLink from './AppLink';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import ToHTML from './ToHTML';
|
import ToHTML from './ToHTML';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
type PuzzleData = {
|
type PuzzleData = {
|
||||||
answer: string;
|
answer: string;
|
||||||
|
@ -21,19 +24,13 @@ type Granted = {
|
||||||
score?: number;
|
score?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
|
export default function Puzzle({ token, id }: { token: string; id: number }) {
|
||||||
if (!puzzle) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [granted, setGranted] = useState<Granted | null>(null);
|
const [granted, setGranted] = useState<Granted | null>(null);
|
||||||
|
|
||||||
const {
|
const { data: puzzle, isLoading } = usePuzzle({ token, id });
|
||||||
register,
|
const { mutate } = useSWRConfig();
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
const { register, handleSubmit } = useForm<PuzzleData>({
|
||||||
setError
|
|
||||||
} = useForm<PuzzleData>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
answer: ''
|
answer: ''
|
||||||
// filename: '',
|
// filename: '',
|
||||||
|
@ -49,11 +46,11 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
formData.append('answer', data.answer.trim());
|
formData.append('answer', data.answer);
|
||||||
// formData.append('filename', 'placeholder');
|
// formData.append('filename', 'placeholder');
|
||||||
// formData.append('code_file', new Blob(), 'placeholder');
|
// formData.append('code_file', new Blob(), 'placeholder');
|
||||||
|
|
||||||
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: {
|
headers: {
|
||||||
|
@ -62,40 +59,47 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok || res.status === 406) {
|
if (res.ok || res.status === 406) {
|
||||||
|
mutate(`puzzles/${puzzle?.id}`);
|
||||||
setGranted(await res.json());
|
setGranted(await res.json());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!puzzle && isLoading) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!puzzle) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<h1 className="text-2xl font-bold sm:text-3xl md:text-4xl">{puzzle.name}</h1>
|
||||||
<h2 className="text-xl font-bold sm:text-2xl md:text-3xl">{puzzle.name}</h2>
|
|
||||||
{/* <p className="text-sm text-muted">Chapitre</p> */}
|
|
||||||
</div>
|
|
||||||
<div className="flex h-screen w-full overflow-y-auto">
|
<div className="flex h-screen w-full overflow-y-auto">
|
||||||
<ToHTML className="font-code text-xs sm:text-base" data={puzzle.content} />
|
<ToHTML className="font-code text-xs sm:text-base" data={puzzle.content} />
|
||||||
</div>
|
</div>
|
||||||
<form
|
{!puzzle.score ? (
|
||||||
className="flex w-full flex-col justify-between sm:flex-row"
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
className="flex w-full flex-col justify-between sm:flex-row"
|
||||||
encType="multipart/form-data"
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
>
|
encType="multipart/form-data"
|
||||||
<div className="flex flex-col items-center justify-center space-x-0 sm:flex-row sm:space-x-6">
|
>
|
||||||
<Input
|
<div className="flex flex-col items-center justify-center space-x-0 sm:flex-row sm:space-x-6">
|
||||||
className="w-full"
|
<Input
|
||||||
label="Réponse"
|
className="w-full"
|
||||||
type="text"
|
label="Réponse"
|
||||||
placeholder="12"
|
type="text"
|
||||||
required
|
placeholder="12"
|
||||||
{...register('answer')}
|
required
|
||||||
/>
|
{...register('answer')}
|
||||||
{granted && (
|
/>
|
||||||
<div className="flex flex-col">
|
{granted && (
|
||||||
<p className="text-sm text-muted">Tentatives actuelles : {granted.tries}</p>
|
<div className="flex flex-col">
|
||||||
{granted.score && <p className="text-sm text-muted">Score : {granted.score}</p>}
|
<p className="text-sm text-muted">Tentatives actuelles : {granted.tries}</p>
|
||||||
</div>
|
{granted.score && <p className="text-sm text-muted">Score : {granted.score}</p>}
|
||||||
)}
|
</div>
|
||||||
{/* <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"
|
||||||
|
@ -103,11 +107,21 @@ export default function Puzzle({ puzzle }: { puzzle: PuzzleType }) {
|
||||||
accept=".py,.js,.ts,.java,.rs,.c"
|
accept=".py,.js,.ts,.java,.rs,.c"
|
||||||
{...register('code_file')}
|
{...register('code_file')}
|
||||||
/> */}
|
/> */}
|
||||||
|
</div>
|
||||||
|
<Button kind="brand" className="mt-6" type="submit">
|
||||||
|
Envoyer
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm text-highlight-secondary">
|
||||||
|
Score : {puzzle.score} (Score maximum: {puzzle.scoreMax})
|
||||||
|
</p>
|
||||||
|
<AppLink href="/dashboard/puzzles" className="text-sm text-highlight-secondary">
|
||||||
|
Retour aux puzzles
|
||||||
|
</AppLink>
|
||||||
</div>
|
</div>
|
||||||
<Button kind="brand" className="mt-6" type="submit">
|
)}
|
||||||
Envoyer
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { UserContext } from '@/context/user';
|
import { UserContext } from '@/context/user';
|
||||||
import { useGroups } from '@/lib/hooks/use-groups';
|
import { useGroups } from '@/lib/hooks/use-groups';
|
||||||
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
||||||
import type { Chapter, Puzzle, Tag } from '@/lib/puzzles';
|
import type { Chapter, Puzzle } from '@/lib/puzzles';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
@ -14,11 +14,11 @@ import Icon from './Icon';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
export default function Puzzles({ token }: { token: string }) {
|
export default function Puzzles({ token }: { token: string }) {
|
||||||
const { data: me } = useContext(UserContext);
|
const { data: me } = useContext(UserContext);
|
||||||
const { data, isLoading } = usePuzzles({ token });
|
const { data, isLoading } = usePuzzles({ token });
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean[]>([]);
|
const [isOpen, setIsOpen] = useState<boolean[]>([]);
|
||||||
|
|
||||||
function handleClick(index: number) {
|
function handleClick(index: number) {
|
||||||
|
@ -44,14 +44,11 @@ export default function Puzzles({ token }: { token: string }) {
|
||||||
<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 flex-col justify-between lg:flex-row lg:items-center">
|
<div className="flex flex-col justify-between lg:flex-row lg:items-center">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h3 className="text-xl font-semibold sm:text-2xl">
|
<h1 className="text-xl font-semibold sm:text-2xl">{chapter.name}</h1>
|
||||||
Chapitre {chapter.id} - {chapter.name}{' '}
|
|
||||||
</h3>
|
|
||||||
{!isInEventGroup(chapter) && (
|
{!isInEventGroup(chapter) && (
|
||||||
<Dialog
|
<Dialog
|
||||||
key={chapter.id}
|
key={chapter.id}
|
||||||
title={chapter.name}
|
title={chapter.name}
|
||||||
tooltip="Select Hogwarts Level"
|
|
||||||
open={isOpen[chapter.id]}
|
open={isOpen[chapter.id]}
|
||||||
onOpenChange={() => handleClick(chapter.id)}
|
onOpenChange={() => handleClick(chapter.id)}
|
||||||
trigger={
|
trigger={
|
||||||
|
@ -96,24 +93,24 @@ export default function Puzzles({ token }: { token: string }) {
|
||||||
{chapter.puzzles &&
|
{chapter.puzzles &&
|
||||||
chapter.puzzles
|
chapter.puzzles
|
||||||
.sort((p1, p2) => {
|
.sort((p1, p2) => {
|
||||||
if (p1.tags == undefined) return 1;
|
const p1Tags = p1.tags || [];
|
||||||
if (p2.tags == undefined) return -1;
|
const p2Tags = p2.tags || [];
|
||||||
const e1 = p1.tags.findIndex((tag) => tag.name === 'easy') >= 0;
|
const p1Easy = p1Tags.some((tag) => tag.name === 'easy');
|
||||||
const e2 = p2.tags.findIndex((tag) => tag.name === 'easy') >= 0;
|
const p2Easy = p2Tags.some((tag) => tag.name === 'easy');
|
||||||
if (e1 && e2) return p1.tags.length - p2.tags.length;
|
if (p1Easy !== p2Easy) {
|
||||||
if (e1) return -1;
|
return p1Easy ? -1 : 1;
|
||||||
if (e2) return 1;
|
}
|
||||||
const m1 = p1.tags.findIndex((tag) => tag.name === 'medium') >= 0;
|
const p1Medium = p1Tags.some((tag) => tag.name === 'medium');
|
||||||
const m2 = p2.tags.findIndex((tag) => tag.name === 'medium') >= 0;
|
const p2Medium = p2Tags.some((tag) => tag.name === 'medium');
|
||||||
if (m1 && m2) return p1.tags.length - p2.tags.length;
|
if (p1Medium !== p2Medium) {
|
||||||
if (m1) return -1;
|
return p1Medium ? -1 : 1;
|
||||||
if (m2) return 1;
|
}
|
||||||
const h1 = p1.tags.findIndex((tag) => tag.name === 'hard') >= 0;
|
const p1Hard = p1Tags.some((tag) => tag.name === 'hard');
|
||||||
const h2 = p2.tags.findIndex((tag) => tag.name === 'hard') >= 0;
|
const p2Hard = p2Tags.some((tag) => tag.name === 'hard');
|
||||||
if (h1 && h2) return p1.tags.length - p2.tags.length;
|
if (p1Hard !== p2Hard) {
|
||||||
if (h1) return -1;
|
return p1Hard ? -1 : 1;
|
||||||
if (h2) return 1;
|
}
|
||||||
return p1.tags.length - p2.tags.length;
|
return p1Tags.length - p2Tags.length;
|
||||||
})
|
})
|
||||||
.map((puzzle) => (
|
.map((puzzle) => (
|
||||||
<PuzzleProp key={puzzle.id} puzzle={puzzle} chapter={chapter} />
|
<PuzzleProp key={puzzle.id} puzzle={puzzle} chapter={chapter} />
|
||||||
|
@ -241,17 +238,13 @@ type GroupData = {
|
||||||
|
|
||||||
function GroupForm({ chapter, token }: { chapter: Chapter; token: string }) {
|
function GroupForm({ chapter, token }: { chapter: Chapter; token: string }) {
|
||||||
const [isJoining, setIsJoining] = useState(false);
|
const [isJoining, setIsJoining] = useState(false);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { data: groups } = useGroups({ token });
|
const { data: groups } = useGroups({ token });
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
const {
|
const router = useRouter();
|
||||||
register,
|
|
||||||
handleSubmit,
|
const { register, handleSubmit, reset } = useForm<GroupData>({
|
||||||
formState: { errors },
|
|
||||||
setError,
|
|
||||||
reset
|
|
||||||
} = useForm<GroupData>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
chapter: chapter.id,
|
chapter: chapter.id,
|
||||||
|
@ -260,20 +253,22 @@ function GroupForm({ chapter, token }: { chapter: Chapter; token: string }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(data: GroupData) {
|
async function onSubmit(data: GroupData) {
|
||||||
const res = await fetch(
|
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/${isJoining ? 'groupJoin' : 'groupCreate'}`, {
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/${isJoining ? 'groupJoin' : 'groupCreate'}`,
|
method: 'POST',
|
||||||
{
|
body: JSON.stringify(data),
|
||||||
method: 'POST',
|
headers: {
|
||||||
body: JSON.stringify(data),
|
Authorization: `Bearer ${token}`
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
// TODO: handle errors
|
// TODO: handle errors
|
||||||
if (res.ok) {
|
// if (res.ok) {
|
||||||
router.refresh();
|
// if (!isJoining) {
|
||||||
}
|
// mutate('groups');
|
||||||
|
// } else {
|
||||||
|
// mutate('me');
|
||||||
|
// }
|
||||||
|
// router.refresh();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -131,7 +131,7 @@ export default function UserAuthForm() {
|
||||||
<Input
|
<Input
|
||||||
label="Nom d'utilisateur"
|
label="Nom d'utilisateur"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="CZ"
|
placeholder="CW"
|
||||||
required
|
required
|
||||||
error={errors.pseudo?.message}
|
error={errors.pseudo?.message}
|
||||||
{...register('pseudo')}
|
{...register('pseudo')}
|
||||||
|
|
|
@ -21,7 +21,15 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex w-full justify-center p-[9px]">
|
<div className="flex w-full justify-center p-[9px]">
|
||||||
<AppLink href="/">
|
<AppLink href="/">
|
||||||
<Image src="/assets/brand/peerat.png" alt="Peer-at" width={50} height={50} />
|
<Image
|
||||||
|
title="Logo"
|
||||||
|
src="/assets/brand/peerat.png"
|
||||||
|
alt="Peer-at"
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
loading="eager"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
</AppLink>
|
</AppLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
|
@ -56,10 +64,10 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
||||||
<li>
|
<li>
|
||||||
<NavItem
|
<NavItem
|
||||||
item={{
|
item={{
|
||||||
name: 'Tutoriels',
|
name: 'Git',
|
||||||
slug: 'tutorials',
|
slug: 'https://git.peerat.dev/Peer-at-Code',
|
||||||
icon: 'question-line',
|
icon: 'git-repository-line',
|
||||||
disabled: true
|
disabled: false
|
||||||
}}
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
|
@ -99,7 +107,6 @@ function NavItem({
|
||||||
'justify-start sm:justify-center': !isOpen
|
'justify-start sm:justify-center': !isOpen
|
||||||
})}
|
})}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
passHref
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Icon className="text-2xl" name={item.icon} />
|
<Icon className="text-2xl" name={item.icon} />
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Usernav from './Usernav';
|
||||||
export default function Wrapper({ children }: { children: ReactNode }) {
|
export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const toggle = () => setIsOpen(!isOpen);
|
const toggle = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<Sidenav isOpen={isOpen} toggle={toggle} />
|
<Sidenav isOpen={isOpen} toggle={toggle} />
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default function EventLeaderboard({ id }: { token: string; id: number })
|
||||||
return () => socket.close();
|
return () => socket.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error } = useSWRSubscription(
|
const { data } = useSWRSubscription(
|
||||||
`wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${id}`,
|
`wss://${process.env.NEXT_PUBLIC_API_URL?.split('//')[1]}/rleaderboard/${id}`,
|
||||||
subscription
|
subscription
|
||||||
);
|
);
|
||||||
|
@ -65,12 +65,12 @@ export default function EventLeaderboard({ id }: { token: string; id: number })
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex flex-col">
|
{/* <div className="flex flex-col">
|
||||||
<span className="text-sm font-semibold">Essaies</span>
|
<span className="text-sm font-semibold">Puzzles</span>
|
||||||
<span className="text-lg text-muted">
|
<span className="text-lg text-muted">
|
||||||
{group.players.reduce((a, b) => a + b.tries, 0)}
|
{group.players.reduce((a, b) => a + b.completions, 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div> */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-semibold">Score</span>
|
<span className="text-sm font-semibold">Score</span>
|
||||||
<span className="text-lg text-muted">
|
<span className="text-lg text-muted">
|
||||||
|
|
|
@ -5,8 +5,6 @@ export default function Podium({ score }: { score: any }) {
|
||||||
.reduce((podiumOrder, position) => [...podiumOrder, score[position]] as any, [])
|
.reduce((podiumOrder, position) => [...podiumOrder, score[position]] as any, [])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
console.log(podium);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mt-8 grid grid-flow-col-dense place-content-center content-end items-end justify-center justify-items-center gap-2"
|
className="mt-8 grid grid-flow-col-dense place-content-center content-end items-end justify-center justify-items-center gap-2"
|
||||||
|
|
Loading…
Add table
Reference in a new issue