chore: Dockerfile & other shits

This commit is contained in:
Théo 2023-07-19 12:32:39 +02:00
parent 08ef306495
commit ae714729bd
29 changed files with 1243 additions and 1344 deletions

View file

@ -1,5 +1,7 @@
.git
Dockerfile Dockerfile
.dockerignore
node_modules node_modules
npm-debug.log
*.md *.md
.env.* .next
.git

View file

@ -1,4 +0,0 @@
{
"typescript.tsdk": "node_modules\\.pnpm\\typescript@4.9.5\\node_modules\\typescript\\lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View file

@ -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"]

View file

@ -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 (

View file

@ -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}

View file

@ -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"

View file

@ -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;

View file

@ -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&apos;est ici que vous pouvez modifier votre profil. C&apos;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&apos; ê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>
); );

View file

@ -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'
}; };

View file

@ -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&apos;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 dHELMO dans le but de te donner
lenvie de coder.
<br />
<br />
Je faire de toi un bon développeur...Bzzzz, Crrrr, Pshiiit...
<br />
<br />
Ouch, ça recommence, mes fichiers sources ont é complètement buggé par des professeurs
de la HEPL. Leur but, me rendre complètement obsolète et faire fuir tous les étudiants
dHELMO. Pour le moment je suis encore capable de tapprendre des choses, mais nous
navons 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 !
Joganiserai 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 cest partBIP...BOP...CRRRRK
</p>
</div>
</div>
</div>
);
}

View file

@ -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>

View file

@ -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>
)} )}

View file

@ -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
View 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>
)
};

View file

@ -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 };

View file

@ -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'

View file

@ -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>

View file

@ -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>
)} )}

View 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 };

View file

@ -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()!;

View file

@ -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,

View file

@ -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 && (

View file

@ -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>;

View file

@ -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 };

View file

@ -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
} }
]; ];

View file

@ -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;

View file

@ -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

File diff suppressed because it is too large Load diff