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
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
node_modules
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
*.md
|
*.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
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json .
|
COPY pnpm-lock.yaml /app
|
||||||
COPY pnpm-lock.yaml .
|
|
||||||
|
|
||||||
RUN npm install -g pnpm && \
|
RUN pnpm fetch
|
||||||
pnpm install sharp && \
|
|
||||||
pnpm install
|
FROM mitchpash/pnpm AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
COPY . .
|
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
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV PORT 3000
|
ENV HOST=localhost PORT=3000 NODE_ENV=production
|
||||||
|
|
||||||
CMD ["pnpm", "start"]
|
CMD ["node", "server.js"]
|
||||||
|
|
||||||
# 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"]
|
|
|
@ -1,5 +1,4 @@
|
||||||
import UserAuthForm from '@/components/ui/UserAuthForm';
|
import UserAuthForm from '@/components/ui/UserAuthForm';
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useContext } from 'react';
|
import { useUser } from '@/context/user';
|
||||||
|
|
||||||
import { UserContext } from '@/context/user';
|
|
||||||
|
|
||||||
import Badge from '@/components/ui/Badge';
|
import Badge from '@/components/ui/Badge';
|
||||||
|
import { Separator } from '@/components/ui/Separator';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { data: me } = useContext(UserContext);
|
const { data: me, isLoading } = useUser();
|
||||||
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="flex flex-col">
|
<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
|
Vos badges sont affichés ici, vous pouvez les partager avec vos amis
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
<Separator />
|
||||||
<main className="flex flex-col justify-between space-x-0 space-y-4">
|
<main className="flex flex-col justify-between space-x-0 space-y-4">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
{me?.badges ? (
|
{!isLoading && me?.badges ? (
|
||||||
me?.badges.map((badge, i) => (
|
me?.badges.map((badge, i) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={i}
|
key={i}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useContext } from 'react';
|
import { AwardIcon, BarChart2Icon, PieChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
import Card from '@/components/ui/Card';
|
import Card from '@/components/ui/Card';
|
||||||
import { UserContext } from '@/context/user';
|
|
||||||
|
import { useUser } from '@/context/user';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { data: me, isLoading } = useContext(UserContext);
|
const { data: me, isLoading } = useUser();
|
||||||
return (
|
return (
|
||||||
<section className="w-full flex-col space-y-4">
|
<section className="w-full flex-col space-y-4">
|
||||||
<header>
|
<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">
|
<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
|
<Card
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
icon="pie-chart-line"
|
Icon={PieChartIcon}
|
||||||
title="Puzzles résolus"
|
title="Puzzles résolus"
|
||||||
data={me?.completions ?? 0}
|
data={me?.completions ?? 0}
|
||||||
link="/dashboard/puzzles"
|
link="/dashboard/puzzles"
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
icon="award-line"
|
Icon={AwardIcon}
|
||||||
title="Badges obtenus"
|
title="Badges obtenus"
|
||||||
data={me?.badges?.length ?? 'Aucun'}
|
data={me?.badges?.length ?? 'Aucun'}
|
||||||
link="/dashboard/badges"
|
link="/dashboard/badges"
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
icon="bar-chart-line"
|
Icon={BarChart2Icon}
|
||||||
title="Rang actuel"
|
title="Rang actuel"
|
||||||
data={me?.rank ?? 'Non classé'}
|
data={me?.rank ?? 'Non classé'}
|
||||||
link="/dashboard/leaderboard"
|
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 type { Metadata } from 'next';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { notFound } from 'next/navigation';
|
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> {
|
export async function generateMetadata({ params }: { params: { id: number } }): Promise<Metadata> {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const token = cookies().get('token')?.value;
|
const token = cookies().get('token')?.value;
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import cookies from 'js-cookie';
|
import cookies from 'js-cookie';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useContext, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useSWRConfig } from 'swr';
|
import { useSWRConfig } from 'swr';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -17,10 +19,10 @@ import {
|
||||||
FormMessage
|
FormMessage
|
||||||
} from '@/components/ui/Form';
|
} from '@/components/ui/Form';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { UserContext } from '@/context/user';
|
|
||||||
|
import { useUser } from '@/context/user';
|
||||||
import { type Player } from '@/lib/players';
|
import { type Player } from '@/lib/players';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { Separator } from '@/components/ui/Separator';
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
|
|
||||||
type SettingsData = {
|
type SettingsData = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -29,16 +31,9 @@ type SettingsData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { data: me } = useContext(UserContext);
|
const { data: me, isLoading } = useUser();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { register, handleSubmit } = useForm<SettingsData>({
|
|
||||||
defaultValues: {
|
|
||||||
name: '',
|
|
||||||
chapter: undefined,
|
|
||||||
puzzle: undefined
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const groups =
|
const groups =
|
||||||
(me?.groups &&
|
(me?.groups &&
|
||||||
|
@ -79,16 +74,24 @@ export default function Page() {
|
||||||
C'est ici que vous pouvez modifier votre profil.
|
C'est ici que vous pouvez modifier votre profil.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
<Separator />
|
||||||
<main className="flex-col space-y-4">
|
<main className="flex-col space-y-4">
|
||||||
{me && <ProfileForm user={me!} />}
|
{!isLoading && <ProfileForm user={me!} />}
|
||||||
{/* {me && me?.groups?.length > 0 ? (
|
{/* <form className="flex w-1/4 flex-col space-y-2" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<form className="flex w-1/4 flex-col space-y-2" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Select label="Quitter un groupe" {...register('name')} options={groups} />
|
<Select label="Quitter un groupe" {...register('name')} options={groups} />
|
||||||
<Button type="submit">Quitter</Button>
|
<Button type="submit">Quitter</Button>
|
||||||
</form>
|
</form> */}
|
||||||
) : (
|
{me?.groups &&
|
||||||
<p>Vous n' êtes dans aucun groupe</p>
|
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>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import '@/styles/global.css';
|
import '@/app/global.css';
|
||||||
|
|
||||||
import { type Metadata } from 'next';
|
import { type Metadata } from 'next';
|
||||||
import { Fira_Code } from 'next/font/google';
|
import { Fira_Code } from 'next/font/google';
|
||||||
|
@ -47,6 +47,20 @@ export const metadata: Metadata = {
|
||||||
shortcut: getURL('/favicon.ico'),
|
shortcut: getURL('/favicon.ico'),
|
||||||
apple: getURL('/assets/icons/apple-touch-icon.png')
|
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'
|
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 { getURL } from '@/lib/utils';
|
||||||
|
|
||||||
import AppLink from '@/components/ui/AppLink';
|
import AppLink from '@/components/ui/AppLink';
|
||||||
import { Icon } from '@/components/ui/Icon';
|
|
||||||
|
|
||||||
export default function Card({
|
export default function Card({
|
||||||
isLoading,
|
isLoading,
|
||||||
icon,
|
Icon,
|
||||||
title,
|
title,
|
||||||
data,
|
data,
|
||||||
link
|
link
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
icon: string;
|
Icon: LucideIcon;
|
||||||
title: string;
|
title: string;
|
||||||
data: any;
|
data: string | number;
|
||||||
link?: string;
|
link?: string;
|
||||||
}) {
|
}) {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return (
|
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">
|
<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">
|
<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-32 animate-pulse rounded bg-highlight-primary" />
|
||||||
<span className="h-[18px] w-24 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 (
|
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">
|
<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 w-full items-center justify-between">
|
||||||
<div className="flex-col">
|
<div className="flex-col">
|
||||||
<h2 className="text-xl font-semibold">{data}</h2>
|
<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"
|
className="text-highlight-secondary transition-colors duration-150 hover:text-brand"
|
||||||
href={getURL(link)}
|
href={getURL(link)}
|
||||||
>
|
>
|
||||||
<Icon name="arrow-right-line" />
|
<ChevronRightIcon className="h-6 w-6" />
|
||||||
</AppLink>
|
</AppLink>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default function Dialog({
|
||||||
<div className="flex w-full justify-between pb-4">
|
<div className="flex w-full justify-between pb-4">
|
||||||
{title}
|
{title}
|
||||||
<DialogPrimitive.Trigger>
|
<DialogPrimitive.Trigger>
|
||||||
<Icon name='close-line' className="hover:text-highlight-secondary" />
|
<Icon name="close-line" className="hover:text-highlight-secondary" />
|
||||||
</DialogPrimitive.Trigger>
|
</DialogPrimitive.Trigger>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export function Icon({ name, className }: { name: string; className?: string }) {
|
export function Icon({ name, className }: { name: string; className?: string }) {
|
||||||
return <i className={cn(`ri-${name}`, className)} role="img" />;
|
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 React from 'react';
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const labelVariants = cva(
|
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<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||||
VariantProps<typeof labelVariants>
|
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||||
ref={ref}
|
));
|
||||||
className={cn(labelVariants(), className)}
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
{...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 Podium from '@/components/ui/events/podium/Podium';
|
||||||
import { useLeaderboardEvent } from '@/lib/hooks/use-leaderboard';
|
import { useLeaderboardEvent } from '@/lib/hooks/use-leaderboard';
|
||||||
import { cn } from '@/lib/utils';
|
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'];
|
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>
|
<p className="text-muted">Suivez la progression des élèves en direct</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<Separator />
|
||||||
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
<main className="flex flex-col justify-between space-x-0 space-y-4 pb-4">
|
||||||
{data && <Podium score={scores} />}
|
{data && <Podium score={scores} />}
|
||||||
{data && data.end_date && (
|
{data && data.end_date && (
|
||||||
|
@ -72,7 +74,7 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-highlight-secondary font-semibold',
|
'font-semibold text-highlight-secondary',
|
||||||
SCORE_COLORS[group.rank - 1]
|
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 items-center space-x-2">
|
||||||
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
|
<div className="flex flex-col gap-x-2 sm:flex-row sm:items-center">
|
||||||
<span className="text-lg">{group.name}</span>
|
<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 && group.players.length > 1
|
||||||
? group.players
|
? group.players
|
||||||
.map((player) => player.pseudo || 'Anonyme')
|
.map((player) => player.pseudo || 'Anonyme')
|
||||||
|
@ -99,11 +101,11 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-semibold">Essai{tries > 1 ? 's' : ''}</span>
|
<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>
|
||||||
<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-highlight-secondary text-lg">
|
<span className="text-lg text-highlight-secondary">
|
||||||
{group.players.reduce((a, b) => a + b.score, 0)}
|
{group.players.reduce((a, b) => a + b.score, 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,7 +116,7 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
[...Array(20).keys()].map((i) => (
|
[...Array(20).keys()].map((i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
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={{
|
style={{
|
||||||
animationDelay: `${i * 0.05}s`,
|
animationDelay: `${i * 0.05}s`,
|
||||||
animationDuration: '1s'
|
animationDuration: '1s'
|
||||||
|
|
|
@ -12,25 +12,14 @@ import { usePuzzle } from '@/lib/hooks/use-puzzles';
|
||||||
import { getURL } from '@/lib/utils';
|
import { getURL } from '@/lib/utils';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import {
|
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/Form';
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage
|
|
||||||
} from '@/components/ui/Form';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import ToHTML from '@/components/ui/ToHTML';
|
import ToHTML from '@/components/ui/ToHTML';
|
||||||
|
|
||||||
import { UserContext } from '@/context/user';
|
import { UserContext } from '@/context/user';
|
||||||
import { useToast } from '@/lib/hooks/use-toast';
|
import { type Puzzle } from '@/lib/puzzles';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
type PuzzleData = {
|
import { Separator } from '@/components/ui/Separator';
|
||||||
answer: string;
|
|
||||||
// filename: string;
|
|
||||||
// code_file: File[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Granted = {
|
type Granted = {
|
||||||
tries: number | null;
|
tries: number | null;
|
||||||
|
@ -41,52 +30,9 @@ type Granted = {
|
||||||
|
|
||||||
export default function Puzzle({ token, id }: { token: string; id: number }) {
|
export default function Puzzle({ token, id }: { token: string; id: number }) {
|
||||||
const { data: me } = useContext(UserContext);
|
const { data: me } = useContext(UserContext);
|
||||||
const [granted, setGranted] = useState<Granted | null>(null);
|
|
||||||
const { data: puzzle, isLoading } = usePuzzle({ token, id });
|
const { data: puzzle, isLoading } = usePuzzle({ token, id });
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
const router = useRouter();
|
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) {
|
if (!puzzle && isLoading) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -106,11 +52,12 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
|
||||||
{puzzle.name}{' '}
|
{puzzle.name}{' '}
|
||||||
<span className="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
<span className="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
<Separator />
|
||||||
<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>
|
||||||
{!puzzle.score ? (
|
{!puzzle.score ? (
|
||||||
<InputForm />
|
<InputForm puzzle={puzzle} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="items-center gap-x-2">
|
<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({
|
const InputFormSchema = z.object({
|
||||||
answer: z.string().nonempty().trim(),
|
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>>({
|
const form = useForm<z.infer<typeof InputFormSchema>>({
|
||||||
resolver: zodResolver(InputFormSchema),
|
resolver: zodResolver(InputFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
answer: '',
|
answer: '',
|
||||||
code_file: null
|
code_file: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(data: z.infer<typeof InputFormSchema>) {
|
const { mutate } = useSWRConfig();
|
||||||
console.log(data);
|
|
||||||
|
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();
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +166,18 @@ function InputForm() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
Envoyer
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
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 { useForm } from 'react-hook-form';
|
||||||
import { useSWRConfig } from 'swr';
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
import AppLink from '@/components/ui/AppLink';
|
import AppLink from '@/components/ui/AppLink';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import Dialog from '@/components/ui/Dialog';
|
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 { Input } from '@/components/ui/Input';
|
||||||
import Select from '@/components/ui/Select';
|
import Select from '@/components/ui/Select';
|
||||||
|
|
||||||
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 useLocalStorage from '@/lib/hooks/use-local-storage';
|
||||||
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
||||||
import type { Chapter, Puzzle } from '@/lib/puzzles';
|
import type { Chapter, Puzzle } from '@/lib/puzzles';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import useLocalStorage from '@/lib/hooks/use-local-storage';
|
import { ChevronRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
const difficulty = [
|
const difficulty = [
|
||||||
{ value: 'easy', label: 'Facile' },
|
{ value: 'easy', label: 'Facile' },
|
||||||
|
@ -263,10 +264,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Icon
|
<ChevronRightIcon className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand" />
|
||||||
className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand"
|
|
||||||
name="arrow-right-line"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</AppLink>
|
</AppLink>
|
||||||
) : (
|
) : (
|
||||||
|
@ -291,10 +289,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* <Icon
|
<ChevronRightIcon className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0" />
|
||||||
className="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0"
|
|
||||||
name="arrow-right-line"
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
</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';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
|
||||||
const AuthFormSchema = z.object({
|
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(),
|
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(),
|
firstname: z.string().optional(),
|
||||||
lastname: 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 router = useRouter();
|
||||||
const pathname = usePathname()!;
|
const pathname = usePathname()!;
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import AppLink from '@/components/ui/AppLink';
|
import { Github } from 'lucide-react';
|
||||||
import { Icon } from '@/components/ui/Icon';
|
|
||||||
import { NavItem, navItems } from '@/lib/nav-items';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
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 }) {
|
export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
|
@ -52,7 +55,7 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
||||||
item={{
|
item={{
|
||||||
name: 'Discord',
|
name: 'Discord',
|
||||||
slug: 'https://discord.gg/72vuHcwUkE',
|
slug: 'https://discord.gg/72vuHcwUkE',
|
||||||
icon: 'discord-fill',
|
icon: Icons.Discord,
|
||||||
disabled: false
|
disabled: false
|
||||||
}}
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
@ -64,7 +67,7 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
||||||
item={{
|
item={{
|
||||||
name: 'Git',
|
name: 'Git',
|
||||||
slug: 'https://git.peerat.dev/Peer-at-Code',
|
slug: 'https://git.peerat.dev/Peer-at-Code',
|
||||||
icon: 'github-fill',
|
icon: Github,
|
||||||
disabled: false
|
disabled: false
|
||||||
}}
|
}}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
@ -109,7 +112,7 @@ function NavItem({
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Icon name={item.icon} />
|
<item.icon />
|
||||||
<span
|
<span
|
||||||
className={cn('hidden lg:block', {
|
className={cn('hidden lg:block', {
|
||||||
'block sm:hidden': isOpen,
|
'block sm:hidden': isOpen,
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
'use client';
|
'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 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 }) {
|
export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segment = useSelectedLayoutSegment();
|
const segment = useSelectedLayoutSegment();
|
||||||
|
|
||||||
const { data: me, isLoading } = useContext(UserContext);
|
const { data: me, isLoading } = useUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
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 flex-row items-center space-x-2 sm:space-x-0">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button onClick={toggle} className="block sm:hidden">
|
<button onClick={toggle} className="block sm:hidden">
|
||||||
{isOpen ? <Icon name="close-line" /> : <Icon name="menu-2-line" />}
|
{isOpen ? (
|
||||||
|
<X size={20} className="text-muted" />
|
||||||
|
) : (
|
||||||
|
<AlignLeftIcon size={20} className="text-muted" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{segment && (
|
{segment && (
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useMe } from '@/lib/hooks/use-players';
|
import { useMe } from '@/lib/hooks/use-players';
|
||||||
import type { Player } from '@/lib/players';
|
import type { Player } from '@/lib/players';
|
||||||
import { createContext, type ReactNode } from 'react';
|
import { createContext, useContext, type ReactNode } from 'react';
|
||||||
|
|
||||||
export const UserContext = createContext<{
|
export const UserContext = createContext<{
|
||||||
data: Player | null | undefined;
|
data: Player | null | undefined;
|
||||||
|
@ -14,6 +14,19 @@ export const UserContext = createContext<{
|
||||||
error: null
|
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 }) => {
|
export const UserProvider = ({ token, children }: { token: string; children: ReactNode }) => {
|
||||||
const { data, isLoading, error } = useMe({ token });
|
const { data, isLoading, error } = useMe({ token });
|
||||||
return <UserContext.Provider value={{ data, isLoading, error }}>{children}</UserContext.Provider>;
|
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 { Award, BarChart2, Code, LayoutDashboard, Settings2, type LucideIcon } from 'lucide-react';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A navigation item.
|
* A navigation item.
|
||||||
|
@ -13,7 +12,7 @@ import { ReactNode } from 'react';
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
icon: string;
|
icon: LucideIcon | React.JSXElementConstructor<React.SVGProps<SVGSVGElement>>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,31 +25,31 @@ export const navItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
slug: 'dashboard',
|
slug: 'dashboard',
|
||||||
icon: 'dashboard-line',
|
icon: LayoutDashboard,
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Classement',
|
name: 'Classement',
|
||||||
slug: 'dashboard/leaderboard',
|
slug: 'dashboard/leaderboard',
|
||||||
icon: 'line-chart-line',
|
icon: BarChart2,
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Puzzles',
|
name: 'Puzzles',
|
||||||
slug: 'dashboard/puzzles',
|
slug: 'dashboard/puzzles',
|
||||||
icon: 'code-s-slash-line',
|
icon: Code,
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Badges',
|
name: 'Badges',
|
||||||
slug: 'dashboard/badges',
|
slug: 'dashboard/badges',
|
||||||
icon: 'award-fill',
|
icon: Award,
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Paramètres',
|
name: 'Paramètres',
|
||||||
slug: 'dashboard/settings',
|
slug: 'dashboard/settings',
|
||||||
icon: 'equalizer-line',
|
icon: Settings2,
|
||||||
disabled: false
|
disabled: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
|
output: 'standalone',
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true
|
||||||
|
},
|
||||||
redirects: async () => {
|
redirects: async () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -14,37 +18,60 @@ const nextConfig = {
|
||||||
headers: async () => {
|
headers: async () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: '/:path*',
|
source: '/(.*)',
|
||||||
headers: [
|
headers: securityHeaders
|
||||||
{
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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;
|
module.exports = nextConfig;
|
||||||
|
|
74
package.json
74
package.json
|
@ -3,8 +3,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next",
|
||||||
"start": "next start",
|
"start": "node .next/standalone/server.js",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"prettier:format": "prettier --write .",
|
"prettier:format": "prettier --write .",
|
||||||
"prettier:check": "prettier --check \"**/*.{ts,tsx,json}\"",
|
"prettier:check": "prettier --check \"**/*.{ts,tsx,json}\"",
|
||||||
|
@ -20,52 +20,52 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Peer-at-Code/peer-at-code#readme",
|
"homepage": "https://github.com/Peer-at-Code/peer-at-code#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.0.3",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@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-select": "^1.2.2",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.4",
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
"boring-avatars": "^1.7.0",
|
"boring-avatars": "^1.10.1",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^2.0.0",
|
||||||
"edge-csrf": "^1.0.3",
|
"edge-csrf": "^1.0.4",
|
||||||
"framer-motion": "^10.12.4",
|
"framer-motion": "^10.12.22",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.252.0",
|
"lucide-react": "^0.261.0",
|
||||||
"next": "13.4.8",
|
"next": "13.4.10",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.44.2",
|
"react-hook-form": "^7.45.2",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.7",
|
||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^3.0.3",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remixicon": "^3.3.0",
|
"sharp": "^0.32.3",
|
||||||
"sharp": "^0.32.1",
|
"swr": "^2.2.0",
|
||||||
"swr": "^2.0.3",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwindcss-animate": "^1.0.6",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
"@types/js-cookie": "^3.0.3",
|
"@types/js-cookie": "^3.0.3",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "20.4.2",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.2.15",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||||
"@typescript-eslint/parser": "^5.50.0",
|
"@typescript-eslint/parser": "^6.1.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-config-next": "13.3.1",
|
"eslint-config-next": "13.4.10",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.26",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "4.9.5"
|
"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