chore: Dockerfile & other shits
This commit is contained in:
parent
08ef306495
commit
ae714729bd
29 changed files with 1243 additions and 1344 deletions
|
@ -1,5 +1,7 @@
|
|||
.git
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
*.md
|
||||
.env.*
|
||||
.next
|
||||
.git
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules\\.pnpm\\typescript@4.9.5\\node_modules\\typescript\\lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
62
Dockerfile
62
Dockerfile
|
@ -1,47 +1,43 @@
|
|||
FROM node:16-alpine AS builder
|
||||
FROM mitchpash/pnpm AS deps
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
COPY pnpm-lock.yaml /app
|
||||
|
||||
RUN npm install -g pnpm && \
|
||||
pnpm install sharp && \
|
||||
pnpm install
|
||||
RUN pnpm fetch
|
||||
|
||||
FROM mitchpash/pnpm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
RUN pnpm install -r --offline
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm build
|
||||
|
||||
FROM mitchpash/pnpm AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
# Some things are not allowed (see https://github.com/vercel/next.js/issues/38119#issuecomment-1172099259)
|
||||
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOST=localhost PORT=3000 NODE_ENV=production
|
||||
|
||||
CMD ["pnpm", "start"]
|
||||
|
||||
# FROM node:16-alpine AS builder
|
||||
|
||||
# RUN apk add --no-cache libc6-compat
|
||||
|
||||
# WORKDIR /app
|
||||
|
||||
# COPY package.json .
|
||||
# COPY pnpm-lock.yaml .
|
||||
|
||||
# ENV NODE_ENV=production \
|
||||
# PORT=3000
|
||||
|
||||
# RUN npm install -g pnpm && \
|
||||
# pnpm install
|
||||
|
||||
# COPY . .
|
||||
|
||||
# RUN npm build
|
||||
|
||||
# EXPOSE 3000
|
||||
|
||||
# CMD ["pnpm", "start"]
|
||||
CMD ["node", "server.js"]
|
|
@ -1,5 +1,4 @@
|
|||
import UserAuthForm from '@/components/ui/UserAuthForm';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { UserContext } from '@/context/user';
|
||||
import { useUser } from '@/context/user';
|
||||
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import { Separator } from '@/components/ui/Separator';
|
||||
|
||||
export default function Page() {
|
||||
const { data: me } = useContext(UserContext);
|
||||
const { data: me, isLoading } = useUser();
|
||||
return (
|
||||
<section className="flex h-full w-full flex-col space-y-4">
|
||||
<header className="flex flex-col">
|
||||
|
@ -16,9 +15,10 @@ export default function Page() {
|
|||
Vos badges sont affichés ici, vous pouvez les partager avec vos amis
|
||||
</p>
|
||||
</header>
|
||||
<Separator />
|
||||
<main className="flex flex-col justify-between space-x-0 space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
{me?.badges ? (
|
||||
{!isLoading && me?.badges ? (
|
||||
me?.badges.map((badge, i) => (
|
||||
<Badge
|
||||
key={i}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { AwardIcon, BarChart2Icon, PieChartIcon } from 'lucide-react';
|
||||
|
||||
import Card from '@/components/ui/Card';
|
||||
import { UserContext } from '@/context/user';
|
||||
|
||||
import { useUser } from '@/context/user';
|
||||
|
||||
export default function Page() {
|
||||
const { data: me, isLoading } = useContext(UserContext);
|
||||
const { data: me, isLoading } = useUser();
|
||||
return (
|
||||
<section className="w-full flex-col space-y-4">
|
||||
<header>
|
||||
|
@ -17,21 +18,21 @@ export default function Page() {
|
|||
<div className="w-full flex-col justify-between space-x-0 space-y-4 md:flex md:flex-row md:space-x-6 md:space-y-0">
|
||||
<Card
|
||||
isLoading={isLoading}
|
||||
icon="pie-chart-line"
|
||||
Icon={PieChartIcon}
|
||||
title="Puzzles résolus"
|
||||
data={me?.completions ?? 0}
|
||||
link="/dashboard/puzzles"
|
||||
/>
|
||||
<Card
|
||||
isLoading={isLoading}
|
||||
icon="award-line"
|
||||
Icon={AwardIcon}
|
||||
title="Badges obtenus"
|
||||
data={me?.badges?.length ?? 'Aucun'}
|
||||
link="/dashboard/badges"
|
||||
/>
|
||||
<Card
|
||||
isLoading={isLoading}
|
||||
icon="bar-chart-line"
|
||||
Icon={BarChart2Icon}
|
||||
title="Rang actuel"
|
||||
data={me?.rank ?? 'Non classé'}
|
||||
link="/dashboard/leaderboard"
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { getPuzzle } from '@/lib/puzzles';
|
||||
import Puzzle from '@/components/ui/Puzzle';
|
||||
import SWRFallback from '@/components/ui/SWRFallback';
|
||||
import type { Metadata } from 'next';
|
||||
import { cookies } from 'next/headers';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getPuzzle } from '@/lib/puzzles';
|
||||
|
||||
import Puzzle from '@/components/ui/Puzzle';
|
||||
import SWRFallback from '@/components/ui/SWRFallback';
|
||||
|
||||
export async function generateMetadata({ params }: { params: { id: number } }): Promise<Metadata> {
|
||||
const { id } = params;
|
||||
const token = cookies().get('token')?.value;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import cookies from 'js-cookie';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useSWRConfig } from 'swr';
|
||||
import { z } from 'zod';
|
||||
|
@ -17,10 +19,10 @@ import {
|
|||
FormMessage
|
||||
} from '@/components/ui/Form';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { UserContext } from '@/context/user';
|
||||
|
||||
import { useUser } from '@/context/user';
|
||||
import { type Player } from '@/lib/players';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Separator } from '@/components/ui/Separator';
|
||||
|
||||
type SettingsData = {
|
||||
name: string;
|
||||
|
@ -29,16 +31,9 @@ type SettingsData = {
|
|||
};
|
||||
|
||||
export default function Page() {
|
||||
const { data: me } = useContext(UserContext);
|
||||
const { data: me, isLoading } = useUser();
|
||||
const { mutate } = useSWRConfig();
|
||||
const router = useRouter();
|
||||
const { register, handleSubmit } = useForm<SettingsData>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
chapter: undefined,
|
||||
puzzle: undefined
|
||||
}
|
||||
});
|
||||
|
||||
const groups =
|
||||
(me?.groups &&
|
||||
|
@ -79,16 +74,24 @@ export default function Page() {
|
|||
C'est ici que vous pouvez modifier votre profil.
|
||||
</p>
|
||||
</header>
|
||||
<Separator />
|
||||
<main className="flex-col space-y-4">
|
||||
{me && <ProfileForm user={me!} />}
|
||||
{/* {me && me?.groups?.length > 0 ? (
|
||||
<form className="flex w-1/4 flex-col space-y-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isLoading && <ProfileForm user={me!} />}
|
||||
{/* <form className="flex w-1/4 flex-col space-y-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Select label="Quitter un groupe" {...register('name')} options={groups} />
|
||||
<Button type="submit">Quitter</Button>
|
||||
</form>
|
||||
) : (
|
||||
<p>Vous n' êtes dans aucun groupe</p>
|
||||
)} */}
|
||||
</form> */}
|
||||
{me?.groups &&
|
||||
me.groups.map((group, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<h2 className="text-xl font-semibold">{group.name}</h2>
|
||||
<p>
|
||||
Chapitre : {group.chapter} - Puzzle : {group.puzzle}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import '@/styles/global.css';
|
||||
import '@/app/global.css';
|
||||
|
||||
import { type Metadata } from 'next';
|
||||
import { Fira_Code } from 'next/font/google';
|
||||
|
@ -47,6 +47,20 @@ export const metadata: Metadata = {
|
|||
shortcut: getURL('/favicon.ico'),
|
||||
apple: getURL('/assets/icons/apple-touch-icon.png')
|
||||
},
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
nocache: true,
|
||||
noarchive: true,
|
||||
nosnippet: true,
|
||||
notranslate: true,
|
||||
noimageindex: true,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
noimageindex: true
|
||||
}
|
||||
},
|
||||
themeColor: '#110F15'
|
||||
};
|
||||
|
||||
|
|
58
app/page.tsx
58
app/page.tsx
|
@ -1,58 +0,0 @@
|
|||
import AppLink from '@/components/ui/AppLink';
|
||||
import Console from '@/components/ui/Console';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex h-screen w-full">
|
||||
<div className="m-auto flex flex-col space-y-2 p-2">
|
||||
<h1 className="text-center text-6xl font-bold">Bienvenue sur</h1>
|
||||
<span>
|
||||
<Console text="Peer-at Code" className="text-6xl" />
|
||||
</span>
|
||||
<AppLink href="/sign-in">Commencer l'aventure</AppLink>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item-center flex h-screen w-full px-2">
|
||||
<div className="m-auto flex flex-col justify-center md:flex-row">
|
||||
<Image
|
||||
title="Philipz 'Cipher Wolf' Barlow"
|
||||
src="/assets/brand/peerat.png"
|
||||
width={500}
|
||||
height={500}
|
||||
alt="Peer-at Code logo"
|
||||
className="self-center"
|
||||
priority
|
||||
/>
|
||||
<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
|
||||
intelligence artificielle développée par des étudiants d’HELMO dans le but de te donner
|
||||
l’envie de coder.
|
||||
<br />
|
||||
<br />
|
||||
Je faire de toi un bon développeur...Bzzzz, Crrrr, Pshiiit...
|
||||
<br />
|
||||
<br />
|
||||
Ouch, ça recommence, mes fichiers sources ont été complètement buggé par des professeurs
|
||||
de la HEPL. Leur but, me rendre complètement obsolète et faire fuir tous les étudiants
|
||||
d’HELMO. Pour le moment je suis encore capable de t’apprendre des choses, mais nous
|
||||
n’avons pas temps à perdre. Si tu apprends assez vite tu pourras corriger mon code
|
||||
source afin de me sauver et de sauver notre chère école.
|
||||
<br />
|
||||
<br />
|
||||
Il me reste peu de temps pour faire de toi un développeur épanouis et compétent. Je vais
|
||||
te donner des petits défis durant la semaine qui te prépareront à un vrai challenge que
|
||||
je te divulguerai tous les samedis matin. Celui-ci permettra de corriger un bug dans mon
|
||||
code. Plus tu gagneras en compétence plus les challenges deviendront compliqués !
|
||||
J’oganiserai régulièrement des batlles de programmation et cybersécurité afin que toi et
|
||||
tes camarades puissiez prouver votre valeur !
|
||||
<br />
|
||||
<br />
|
||||
Alors prêt à me suivre ? A ton clavier est c’est part…BIP...BOP...CRRRRK…
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,25 +1,25 @@
|
|||
import { ChevronRightIcon, Loader2, type LucideIcon } from 'lucide-react';
|
||||
import { getURL } from '@/lib/utils';
|
||||
|
||||
import AppLink from '@/components/ui/AppLink';
|
||||
import { Icon } from '@/components/ui/Icon';
|
||||
|
||||
export default function Card({
|
||||
isLoading,
|
||||
icon,
|
||||
Icon,
|
||||
title,
|
||||
data,
|
||||
link
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
icon: string;
|
||||
Icon: LucideIcon;
|
||||
title: string;
|
||||
data: any;
|
||||
data: string | number;
|
||||
link?: string;
|
||||
}) {
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md">
|
||||
<Icon name="loader-5" className="h-4 w-4" />
|
||||
<Loader2 className="animate-spin text-2xl text-muted" size={30} />
|
||||
<div className="flex flex-col space-y-4">
|
||||
<span className="h-[18px] w-32 animate-pulse rounded bg-highlight-primary" />
|
||||
<span className="h-[18px] w-24 animate-pulse rounded bg-highlight-primary" />
|
||||
|
@ -29,7 +29,7 @@ export default function Card({
|
|||
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md">
|
||||
<Icon name={icon} className="text-2xl text-muted" />
|
||||
<Icon className="text-muted" size={30} />
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex-col">
|
||||
<h2 className="text-xl font-semibold">{data}</h2>
|
||||
|
@ -40,7 +40,7 @@ export default function Card({
|
|||
className="text-highlight-secondary transition-colors duration-150 hover:text-brand"
|
||||
href={getURL(link)}
|
||||
>
|
||||
<Icon name="arrow-right-line" />
|
||||
<ChevronRightIcon className="h-6 w-6" />
|
||||
</AppLink>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function Dialog({
|
|||
<div className="flex w-full justify-between pb-4">
|
||||
{title}
|
||||
<DialogPrimitive.Trigger>
|
||||
<Icon name='close-line' className="hover:text-highlight-secondary" />
|
||||
<Icon name="close-line" className="hover:text-highlight-secondary" />
|
||||
</DialogPrimitive.Trigger>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Icon({ name, className }: { name: string; className?: string }) {
|
||||
return <i className={cn(`ri-${name}`, className)} role="img" />;
|
||||
|
|
16
components/ui/Icons.tsx
Normal file
16
components/ui/Icons.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
type IconProps = React.HTMLAttributes<SVGSVGElement>;
|
||||
|
||||
export const Icons = {
|
||||
Discord: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
{...props}
|
||||
>
|
||||
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
|
||||
</svg>
|
||||
)
|
||||
};
|
|
@ -1,26 +1,21 @@
|
|||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react';
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
|
|
@ -5,7 +5,8 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||
import Podium from '@/components/ui/events/podium/Podium';
|
||||
import { useLeaderboardEvent } from '@/lib/hooks/use-leaderboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Timer } from './Timer';
|
||||
import { Timer } from '@/components/ui/Timer';
|
||||
import { Separator } from '@/components/ui/Separator';
|
||||
|
||||
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
|
||||
|
||||
|
@ -50,6 +51,7 @@ export default function Leaderboard({ token }: { token: string }) {
|
|||
<p className="text-muted">Suivez la progression des élèves en direct</p>
|
||||
</div>
|
||||
</header>
|
||||
<Separator />
|
||||
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
||||
{data && <Podium score={scores} />}
|
||||
{data && data.end_date && (
|
||||
|
@ -72,7 +74,7 @@ export default function Leaderboard({ token }: { token: string }) {
|
|||
<div className="flex items-center space-x-4">
|
||||
<span
|
||||
className={cn(
|
||||
'text-highlight-secondary font-semibold',
|
||||
'font-semibold text-highlight-secondary',
|
||||
SCORE_COLORS[group.rank - 1]
|
||||
)}
|
||||
>
|
||||
|
@ -81,7 +83,7 @@ export default function Leaderboard({ token }: { token: string }) {
|
|||
<div className="flex items-center space-x-2">
|
||||
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
|
||||
<span className="text-lg">{group.name}</span>
|
||||
<span className="text-highlight-secondary text-sm">
|
||||
<span className="text-sm text-highlight-secondary">
|
||||
{group.players && group.players.length > 1
|
||||
? group.players
|
||||
.map((player) => player.pseudo || 'Anonyme')
|
||||
|
@ -99,11 +101,11 @@ export default function Leaderboard({ token }: { token: string }) {
|
|||
</div> */}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">Essai{tries > 1 ? 's' : ''}</span>
|
||||
<span className="text-highlight-secondary text-lg">{tries}</span>
|
||||
<span className="text-lg text-highlight-secondary">{tries}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">Score</span>
|
||||
<span className="text-highlight-secondary text-lg">
|
||||
<span className="text-lg text-highlight-secondary">
|
||||
{group.players.reduce((a, b) => a + b.score, 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -114,7 +116,7 @@ export default function Leaderboard({ token }: { token: string }) {
|
|||
[...Array(20).keys()].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="bg-primary-600 inline-block h-12 animate-pulse rounded-lg"
|
||||
className="inline-block h-12 animate-pulse rounded-lg bg-primary-600"
|
||||
style={{
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
animationDuration: '1s'
|
||||
|
|
|
@ -12,25 +12,14 @@ import { usePuzzle } from '@/lib/hooks/use-puzzles';
|
|||
import { getURL } from '@/lib/utils';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/Form';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/Form';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import ToHTML from '@/components/ui/ToHTML';
|
||||
|
||||
import { UserContext } from '@/context/user';
|
||||
import { useToast } from '@/lib/hooks/use-toast';
|
||||
|
||||
type PuzzleData = {
|
||||
answer: string;
|
||||
// filename: string;
|
||||
// code_file: File[];
|
||||
};
|
||||
import { type Puzzle } from '@/lib/puzzles';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Separator } from '@/components/ui/Separator';
|
||||
|
||||
type Granted = {
|
||||
tries: number | null;
|
||||
|
@ -41,52 +30,9 @@ type Granted = {
|
|||
|
||||
export default function Puzzle({ token, id }: { token: string; id: number }) {
|
||||
const { data: me } = useContext(UserContext);
|
||||
const [granted, setGranted] = useState<Granted | null>(null);
|
||||
const { data: puzzle, isLoading } = usePuzzle({ token, id });
|
||||
const { mutate } = useSWRConfig();
|
||||
const router = useRouter();
|
||||
|
||||
const { register, handleSubmit } = useForm<PuzzleData>({
|
||||
defaultValues: {
|
||||
answer: ''
|
||||
// filename: '',
|
||||
// code_file: []
|
||||
}
|
||||
});
|
||||
|
||||
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', 'placeholder');
|
||||
// formData.append('code_file', new Blob(), 'placeholder');
|
||||
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzleResponse/${puzzle!.id}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
Authorization: `Bearer ${cookies.get('token')}}`
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok || res.status === 403 || res.status === 406 || res.status === 423) {
|
||||
const data = res.ok || res.status === 406 ? ((await res.json()) as Granted) : null;
|
||||
if (data && data.score) {
|
||||
mutate(`puzzles/${puzzle?.id}`);
|
||||
} else if (data && data.tries) setGranted(data);
|
||||
else if (res.ok && data?.success)
|
||||
setGranted({ tries: null, score: null, message: 'Réponse correcte' });
|
||||
else if (res.status === 423)
|
||||
setGranted({ tries: null, score: null, message: 'Réponse incorrecte' });
|
||||
else if (res.status === 403) mutate(`puzzles/${puzzle?.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!puzzle && isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
@ -106,11 +52,12 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
|
|||
{puzzle.name}{' '}
|
||||
<span className="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
||||
</h1>
|
||||
<Separator />
|
||||
<div className="flex h-screen w-full overflow-y-auto">
|
||||
<ToHTML className="font-code text-xs sm:text-base" data={puzzle.content} />
|
||||
</div>
|
||||
{!puzzle.score ? (
|
||||
<InputForm />
|
||||
<InputForm puzzle={puzzle} />
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="items-center gap-x-2">
|
||||
|
@ -133,20 +80,56 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
|
|||
|
||||
const InputFormSchema = z.object({
|
||||
answer: z.string().nonempty().trim(),
|
||||
code_file: z.any().nullable()
|
||||
code_file: z.any().optional()
|
||||
});
|
||||
|
||||
function InputForm() {
|
||||
function InputForm({ puzzle }: { puzzle?: Puzzle }) {
|
||||
const form = useForm<z.infer<typeof InputFormSchema>>({
|
||||
resolver: zodResolver(InputFormSchema),
|
||||
defaultValues: {
|
||||
answer: '',
|
||||
code_file: null
|
||||
code_file: undefined
|
||||
}
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof InputFormSchema>) {
|
||||
console.log(data);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [granted, setGranted] = useState<Granted | null>(null);
|
||||
|
||||
async function onSubmit(data: z.infer<typeof InputFormSchema>) {
|
||||
setIsLoading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (data.code_file) {
|
||||
formData.append('code_file', data.code_file[0]);
|
||||
}
|
||||
|
||||
formData.append('answer', data.answer);
|
||||
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/puzzleResponse/${puzzle!.id}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
Authorization: `Bearer ${cookies.get('token')}}`
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok || res.status === 403 || res.status === 406 || res.status === 423) {
|
||||
const data = res.ok || res.status === 406 ? ((await res.json()) as Granted) : null;
|
||||
if (data && data.score) {
|
||||
mutate(`puzzles/${puzzle?.id}`);
|
||||
} else if (data && data.tries) setGranted(data);
|
||||
else if (res.ok && data?.success)
|
||||
setGranted({ tries: null, score: null, message: 'Réponse correcte' });
|
||||
else if (res.status === 423)
|
||||
setGranted({ tries: null, score: null, message: 'Réponse incorrecte' });
|
||||
else if (res.status === 403) mutate(`puzzles/${puzzle?.id}`);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
form.reset();
|
||||
}
|
||||
|
||||
|
@ -183,7 +166,18 @@ function InputForm() {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full sm:w-44" variant="brand">
|
||||
{granted && (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<p className="text-brand-accent">{granted.message}</p>
|
||||
{granted.tries && (
|
||||
<p className="text-brand-accent">
|
||||
Il vous reste {granted.tries} tentative{granted.tries > 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button disabled={isLoading} className="w-full sm:w-44" variant="brand">
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Envoyer
|
||||
</Button>
|
||||
</form>
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { type ChangeEvent, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useContext, useEffect, useMemo, useState, type ChangeEvent } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
import AppLink from '@/components/ui/AppLink';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import Dialog from '@/components/ui/Dialog';
|
||||
import { Icon, Icons } from '@/components/ui/Icon';
|
||||
import { Icon } from '@/components/ui/Icon';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import Select from '@/components/ui/Select';
|
||||
|
||||
import { UserContext } from '@/context/user';
|
||||
import { useGroups } from '@/lib/hooks/use-groups';
|
||||
import useLocalStorage from '@/lib/hooks/use-local-storage';
|
||||
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
||||
import type { Chapter, Puzzle } from '@/lib/puzzles';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useLocalStorage from '@/lib/hooks/use-local-storage';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
const difficulty = [
|
||||
{ value: 'easy', label: 'Facile' },
|
||||
|
@ -263,10 +264,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
<Icon
|
||||
className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand"
|
||||
name="arrow-right-line"
|
||||
/>
|
||||
<ChevronRightIcon className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand" />
|
||||
</div>
|
||||
</AppLink>
|
||||
) : (
|
||||
|
@ -291,10 +289,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* <Icon
|
||||
className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0"
|
||||
name="arrow-right-line"
|
||||
/> */}
|
||||
<ChevronRightIcon className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
26
components/ui/Separator.tsx
Normal file
26
components/ui/Separator.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
'use client';
|
||||
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-highlight-primary',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
|
@ -21,9 +21,9 @@ import {
|
|||
import { Input } from '@/components/ui/Input';
|
||||
|
||||
const AuthFormSchema = z.object({
|
||||
pseudo: z.string().min(3, 'Votre pseudo doit faire au moins 3 caractères.'),
|
||||
pseudo: z.string().nonempty({ message: "Nom d'utilisateur requis" }),
|
||||
email: z.string().optional(),
|
||||
passwd: z.string().min(8, 'Votre mot de passe doit faire au moins 8 caractères.'),
|
||||
passwd: z.string().nonempty({ message: 'Mot de passe requis' }),
|
||||
firstname: z.string().optional(),
|
||||
lastname: z.string().optional()
|
||||
});
|
||||
|
@ -40,7 +40,7 @@ export default function UserAuthForm() {
|
|||
}
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname()!;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import AppLink from '@/components/ui/AppLink';
|
||||
import { Icon } from '@/components/ui/Icon';
|
||||
import { NavItem, navItems } from '@/lib/nav-items';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Github } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||
|
||||
import { NavItem, navItems } from '@/lib/nav-items';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import AppLink from '@/components/ui/AppLink';
|
||||
import { Icons } from '@/components/ui/Icons';
|
||||
|
||||
export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||
return (
|
||||
<aside
|
||||
|
@ -52,7 +55,7 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
|||
item={{
|
||||
name: 'Discord',
|
||||
slug: 'https://discord.gg/72vuHcwUkE',
|
||||
icon: 'discord-fill',
|
||||
icon: Icons.Discord,
|
||||
disabled: false
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
|
@ -64,7 +67,7 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
|||
item={{
|
||||
name: 'Git',
|
||||
slug: 'https://git.peerat.dev/Peer-at-Code',
|
||||
icon: 'github-fill',
|
||||
icon: Github,
|
||||
disabled: false
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
|
@ -109,7 +112,7 @@ function NavItem({
|
|||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon name={item.icon} />
|
||||
<item.icon />
|
||||
<span
|
||||
className={cn('hidden lg:block', {
|
||||
'block sm:hidden': isOpen,
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import { UserContext } from '@/context/user';
|
||||
import { titleCase } from '@/lib/utils';
|
||||
import { useRouter, useSelectedLayoutSegment } from 'next/navigation';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import AvatarComponent from '../Avatar';
|
||||
import { Icon, Icons } from '@/components/ui/Icon';
|
||||
import Popover from '@/components/ui/Popover';
|
||||
import { useUser } from '@/context/user';
|
||||
import { titleCase } from '@/lib/utils';
|
||||
import { AlignLeftIcon, X } from 'lucide-react';
|
||||
import { useRouter, useSelectedLayoutSegment } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import AvatarComponent from '../Avatar';
|
||||
|
||||
export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const segment = useSelectedLayoutSegment();
|
||||
|
||||
const { data: me, isLoading } = useContext(UserContext);
|
||||
const { data: me, isLoading } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
|
@ -26,7 +26,11 @@ export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
|||
<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">
|
||||
{isOpen ? <Icon name="close-line" /> : <Icon name="menu-2-line" />}
|
||||
{isOpen ? (
|
||||
<X size={20} className="text-muted" />
|
||||
) : (
|
||||
<AlignLeftIcon size={20} className="text-muted" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{segment && (
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useMe } from '@/lib/hooks/use-players';
|
||||
import type { Player } from '@/lib/players';
|
||||
import { createContext, type ReactNode } from 'react';
|
||||
import { createContext, useContext, type ReactNode } from 'react';
|
||||
|
||||
export const UserContext = createContext<{
|
||||
data: Player | null | undefined;
|
||||
|
@ -14,6 +14,19 @@ export const UserContext = createContext<{
|
|||
error: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom hook to get the user data from the context.
|
||||
*
|
||||
* @returns {Player | null | undefined} The user data.
|
||||
*/
|
||||
export const useUser = () => {
|
||||
const context = useContext(UserContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useUser must be used within a UserProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const UserProvider = ({ token, children }: { token: string; children: ReactNode }) => {
|
||||
const { data, isLoading, error } = useMe({ token });
|
||||
return <UserContext.Provider value={{ data, isLoading, error }}>{children}</UserContext.Provider>;
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
// Inspired by react-hot-toast library
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ToastActionElement, ToastProps } from '@/components/ui/Toast';
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST'
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType['ADD_TOAST'];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType['UPDATE_TOAST'];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType['DISMISS_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
}
|
||||
| {
|
||||
type: ActionType['REMOVE_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
|
||||
};
|
||||
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t))
|
||||
};
|
||||
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false
|
||||
}
|
||||
: t
|
||||
)
|
||||
};
|
||||
}
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: []
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, 'id'>;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ...props, id }
|
||||
});
|
||||
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open: any) => {
|
||||
if (!open) dismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId })
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
|
@ -1,5 +1,4 @@
|
|||
import { Icons, type Icon } from '@/components/ui/Icon';
|
||||
import { ReactNode } from 'react';
|
||||
import { Award, BarChart2, Code, LayoutDashboard, Settings2, type LucideIcon } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* A navigation item.
|
||||
|
@ -13,7 +12,7 @@ import { ReactNode } from 'react';
|
|||
export type NavItem = {
|
||||
name: string;
|
||||
slug: string;
|
||||
icon: string;
|
||||
icon: LucideIcon | React.JSXElementConstructor<React.SVGProps<SVGSVGElement>>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
|
@ -26,31 +25,31 @@ export const navItems: NavItem[] = [
|
|||
{
|
||||
name: 'Dashboard',
|
||||
slug: 'dashboard',
|
||||
icon: 'dashboard-line',
|
||||
icon: LayoutDashboard,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Classement',
|
||||
slug: 'dashboard/leaderboard',
|
||||
icon: 'line-chart-line',
|
||||
icon: BarChart2,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Puzzles',
|
||||
slug: 'dashboard/puzzles',
|
||||
icon: 'code-s-slash-line',
|
||||
icon: Code,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Badges',
|
||||
slug: 'dashboard/badges',
|
||||
icon: 'award-fill',
|
||||
icon: Award,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Paramètres',
|
||||
slug: 'dashboard/settings',
|
||||
icon: 'equalizer-line',
|
||||
icon: Settings2,
|
||||
disabled: false
|
||||
}
|
||||
];
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
output: 'standalone',
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true
|
||||
},
|
||||
redirects: async () => {
|
||||
return [
|
||||
{
|
||||
|
@ -14,37 +18,60 @@ const nextConfig = {
|
|||
headers: async () => {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY'
|
||||
},
|
||||
// {
|
||||
// key: 'Content-Security-Policy',
|
||||
// value:
|
||||
// "connect-src 'self' https://api.peerat.dev wss://api.peerat.dev; default-src 'self'; font-src 'self'; frame-ancestors 'none'; img-src 'self' data:; script-src 'self'; style-src 'self' https://fonts.googleapis.com;"
|
||||
// },
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff'
|
||||
},
|
||||
{
|
||||
key: 'Permissions-Policy',
|
||||
value: 'camera=(), battery=(), geolocation=(), microphone=(), browsing-topics=()'
|
||||
},
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'strict-origin-when-cross-origin'
|
||||
},
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=31536000; includeSubDomains; preload'
|
||||
}
|
||||
]
|
||||
source: '/(.*)',
|
||||
headers: securityHeaders
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// https://nextjs.org/docs/advanced-features/security-headers
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src * blob: data:;
|
||||
media-src 'none';
|
||||
connect-src *;
|
||||
font-src 'self' data:;
|
||||
`;
|
||||
|
||||
const securityHeaders = [
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: ContentSecurityPolicy.replace(/\n/g, '')
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'origin-when-cross-origin'
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY'
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff'
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
|
||||
{
|
||||
key: 'X-DNS-Prefetch-Control',
|
||||
value: 'on'
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=31536000; includeSubDomains; preload'
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
|
||||
{
|
||||
key: 'Permissions-Policy',
|
||||
value: 'camera=(), microphone=(), geolocation=()'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
74
package.json
74
package.json
|
@ -3,8 +3,8 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"build": "next",
|
||||
"start": "node .next/standalone/server.js",
|
||||
"lint": "next lint",
|
||||
"prettier:format": "prettier --write .",
|
||||
"prettier:check": "prettier --check \"**/*.{ts,tsx,json}\"",
|
||||
|
@ -20,52 +20,52 @@
|
|||
},
|
||||
"homepage": "https://github.com/Peer-at-Code/peer-at-code#readme",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
"@hookform/resolvers": "^3.1.1",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.5",
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"boring-avatars": "^1.7.0",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"edge-csrf": "^1.0.3",
|
||||
"framer-motion": "^10.12.4",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lucide-react": "^0.252.0",
|
||||
"next": "13.4.8",
|
||||
"boring-avatars": "^1.10.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"edge-csrf": "^1.0.4",
|
||||
"framer-motion": "^10.12.22",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.261.0",
|
||||
"next": "13.4.10",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.44.2",
|
||||
"react-markdown": "^8.0.5",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"react-hook-form": "^7.45.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark-breaks": "^3.0.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remixicon": "^3.3.0",
|
||||
"sharp": "^0.32.1",
|
||||
"swr": "^2.0.3",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"sharp": "^0.32.3",
|
||||
"swr": "^2.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/forms": "^0.5.4",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-next": "13.3.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "4.9.5"
|
||||
"@types/node": "20.4.2",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-next": "13.4.10",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"postcss": "^8.4.26",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
|
|
1707
pnpm-lock.yaml
generated
1707
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue