diff --git a/.env.template b/.env.template index ec60957..c5c9dc5 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,2 @@ -NEXT_PUBLIC_API_URL=API -NEXT_PUBLIC_SITE_URL=SITE \ No newline at end of file +NEXT_PUBLIC_SITE_URL=SITE +NEXT_PUBLIC_API_URL=API \ No newline at end of file diff --git a/app/dashboard/badges/page.tsx b/app/dashboard/badges/page.tsx index d7d8765..122becb 100644 --- a/app/dashboard/badges/page.tsx +++ b/app/dashboard/badges/page.tsx @@ -1,11 +1,11 @@ -import Badge from '@/ui/Badge'; +'use client'; -export const metadata = { - title: 'Mes badges - Peer-at Code' -}; +import { UserContext } from '@/context/user'; +import Badge from '@/ui/Badge'; +import { useContext } from 'react'; export default function Page() { - // TODO: Fetch badges from API and display them + const { data: me } = useContext(UserContext); return (
@@ -18,15 +18,19 @@ export default function Page() {
- - - + {me?.badges ? ( + me?.badges.map((badge, i) => ( + + )) + ) : ( +

Aucun badge

+ )}
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 7dd6000..8721e6b 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,11 +1,16 @@ import { type ReactNode } from 'react'; +import { UserProvider } from '@/context/user'; import Wrapper from '@/ui/dashboard/Wrapper'; +import { cookies } from 'next/headers'; -export default function Layout({ children }: { children: ReactNode }) { +export default async function Layout({ children }: { children: ReactNode }) { + const token = cookies().get('token')!.value; return (
- {children} + + {children} +
); } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 5d39a7a..2c721fe 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,12 +1,11 @@ 'use client'; -import { useMe } from '@/lib/hooks/use-players'; +import { UserContext } from '@/context/user'; import Card from '@/ui/Card'; -import cookies from 'js-cookie'; +import { useContext } from 'react'; export default function Page() { - const token = cookies.get('token'); - const { data: me, isLoading } = useMe({ token: token! }); + const { data: me, isLoading } = useContext(UserContext); return (
@@ -19,21 +18,16 @@ export default function Page() { - +
diff --git a/context/user.tsx b/context/user.tsx new file mode 100644 index 0000000..d9c1bca --- /dev/null +++ b/context/user.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { useMe } from '@/lib/hooks/use-players'; +import type { Player } from '@/lib/players'; +import { createContext, type ReactNode } from 'react'; + +export const UserContext = createContext<{ + data: Player | null | undefined; + isLoading: boolean; + error: Error | null; +}>({ + data: null, + isLoading: true, + error: null +}); + +export const UserProvider = ({ token, children }: { token: string; children: ReactNode }) => { + const { data, isLoading, error } = useMe({ token }); + return {children}; +}; diff --git a/lib/hooks/use-players.ts b/lib/hooks/use-players.ts index 22bcd73..96c6d5f 100644 --- a/lib/hooks/use-players.ts +++ b/lib/hooks/use-players.ts @@ -2,9 +2,7 @@ import useSWR from 'swr'; import { getPlayer } from '../players'; export function useMe({ token }: { token: string }) { - return useSWR('me', () => getPlayer({ token }), { - revalidateOnReconnect: false - }); + return useSWR('me', () => getPlayer({ token })); } export function usePlayer({ token, username }: { token: string; username: string }) { diff --git a/lib/leaderboard.ts b/lib/leaderboard.ts index 54d79f7..7f32d3b 100644 --- a/lib/leaderboard.ts +++ b/lib/leaderboard.ts @@ -1,4 +1,5 @@ import fetcher from './fetcher'; +import type { Group } from './players'; export const getScores = async ({ token }: { token: string }): Promise => { const { data, status } = await fetcher.get(`/leaderboard`, { @@ -25,6 +26,7 @@ export type Score = { tries: number; completions: number; pseudo: string; - group: string; + groups: Group[]; avatar: string; + rank: number; }; diff --git a/lib/nav-items.ts b/lib/nav-items.ts index c2d7558..1e8cdb7 100644 --- a/lib/nav-items.ts +++ b/lib/nav-items.ts @@ -48,6 +48,6 @@ export const navItems: NavItem[] = [ name: 'Paramètres', slug: 'settings', icon: 'equalizer-line', - disabled: false + disabled: true } ]; diff --git a/lib/players.ts b/lib/players.ts index afdc3be..3e3a584 100644 --- a/lib/players.ts +++ b/lib/players.ts @@ -28,14 +28,26 @@ export const getPlayer = async ({ export type Player = { email: string; + pseudo: string; firstnames: string; lastname: string; description: string; avatar: string; - group: string; + groups: Group[]; score: number; tries: number; completions: number; - pseudo: string; - badges: any[]; + rank: number; + badges: Badge[] | null; +}; + +export type Badge = { + name: string; + level: number; + logo?: string; +}; + +export type Group = { + name: string; + chapter?: number; }; diff --git a/lib/puzzles.ts b/lib/puzzles.ts index 1187721..e2371ec 100644 --- a/lib/puzzles.ts +++ b/lib/puzzles.ts @@ -7,7 +7,7 @@ export const getChapters = async ({ token }: { token: string }): Promise chapter.id !== 0); - return chapters as Chapter[]; }; @@ -48,24 +46,17 @@ export const getChapter = async ({ return chapter as Chapter; }; -export const getPuzzles = async ({ - token -}: { - token: string; -}): Promise<{ chapters: Chapter[]; puzzles: Puzzle[] }> => { +export const getPuzzles = async ({ token }: { token: string }): Promise => { const chapters = await getChapters({ token }); - const puzzles: Puzzle[] = []; - for (const chapter of chapters) { - const puzzlesByChapter = await getChapter({ token, id: chapter.id }); - if (!puzzlesByChapter?.puzzles) continue; - puzzles.push(...puzzlesByChapter!.puzzles); + for (let i = 0; i < chapters.length; i++) { + const chapter = chapters[i]; + const chapterData = await getChapter({ token, id: chapter.id }); + if (!chapterData) continue; + chapters[i].puzzles = chapterData.puzzles; } - return { - chapters: chapters as Chapter[], - puzzles: puzzles as Puzzle[] - }; + return chapters as Chapter[]; }; export const getPuzzle = async ({ token, id }: { token: string; id: number }): Promise => { @@ -92,10 +83,17 @@ export type Puzzle = { id: number; name: string; content: string; + tags: Tag[] | null; }; export type Chapter = { - name: string; id: number; + name: string; puzzles: Puzzle[]; + startDay?: string; + endDay?: string; +}; + +export type Tag = { + name: string; }; diff --git a/middleware.ts b/middleware.ts index 3c074e8..6d1f355 100644 --- a/middleware.ts +++ b/middleware.ts @@ -9,10 +9,6 @@ import { getURL } from './lib/utils'; export async function middleware(req: NextRequest) { const res = NextResponse.next(); - // on donne accès à l'API depuis n'importe quelle origine - res.headers.set('Access-Control-Allow-Origin', '*'); - res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - const token = req.cookies.get('token')?.value; if (req.nextUrl.pathname.includes('dashboard') && !token) diff --git a/package.json b/package.json index 2b806c1..9b21acf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "peer-at-code", - "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", diff --git a/public/assets/brand/peerat.png b/public/assets/brand/peerat.png index d1b2385..f0086c9 100644 Binary files a/public/assets/brand/peerat.png and b/public/assets/brand/peerat.png differ diff --git a/tailwind.config.js b/tailwind.config.js index b160c52..cf483cb 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires const defaultTheme = require('tailwindcss/defaultTheme'); /** @type {import('tailwindcss').Config} */ diff --git a/ui/Badge.tsx b/ui/Badge.tsx index 943afc5..284f42d 100644 --- a/ui/Badge.tsx +++ b/ui/Badge.tsx @@ -2,42 +2,37 @@ import Image from 'next/image'; import { cn } from '@/lib/utils'; -export type Difficulty = 'easy' | 'medium' | 'hard'; - -export const DIFFICULTY = { - 1: 'easy', - 2: 'medium', - 3: 'hard' -} +export const DIFFICULTY_COLOR = { + 1: 'green', + 2: 'yellow', + 3: 'red' +}; export default function Badge({ - title, - path, + name, + src, alt, - type = 'easy', - earned = false + level }: { - title: string; - path: string; + name: string; + src: string; alt: string; - type?: Difficulty; - earned?: boolean; + level: number; }) { return (
{alt} - {earned ? title : '****'} + {name}
); } diff --git a/ui/Input.tsx b/ui/Input.tsx index cdede32..cab7c6b 100644 --- a/ui/Input.tsx +++ b/ui/Input.tsx @@ -1,6 +1,5 @@ import { forwardRef } from 'react'; import ErrorMessage from './ErrorMessage'; -import Icon from './Icon'; import Label from './Label'; const Input = forwardRef< diff --git a/ui/Leaderboard.tsx b/ui/Leaderboard.tsx index 23ab4e0..ea42532 100644 --- a/ui/Leaderboard.tsx +++ b/ui/Leaderboard.tsx @@ -6,22 +6,23 @@ import { useMemo, useState } from 'react'; import AvatarComponent from './Avatar'; import Select from './Select'; -const scoreColors = ['text-yellow-400', 'text-gray-400', 'text-orange-400']; +const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400']; export default function Leaderboard({ token }: { token: string }) { const { data, isLoading } = useLeaderboard({ token }); const [filter, setFilter] = useState(''); - let options; + let options = [] as { value: string; title: string }[]; if (data) { options = data - .filter((score, index, self) => { - return index === self.findIndex((t) => t.group === score.group) && score.group !== ''; - }) - .sort((a, b) => (a.group > b.group ? 1 : -1)) - .map((score) => ({ value: score.group, title: score.group })); + .filter((score) => score.groups && score.groups.length > 0) + .map((score) => score.groups.map((group) => ({ value: group.name, title: group.name }))) + .flat() + .filter((group, index, self) => self.findIndex((g) => g.value === group.value) === index) + .sort((a, b) => a.title.localeCompare(b.title)); + options.unshift({ value: '', title: 'Tous' }); options.push({ value: 'no-group', title: 'Sans groupe' }); } @@ -29,9 +30,9 @@ export default function Leaderboard({ token }: { token: string }) { const filteredData = useMemo(() => { if (filter) { if (filter === 'no-group') { - return data?.filter((score) => score.group === ''); + return data?.filter((score) => !score.groups || score.groups.length === 0); } - return data?.filter((score) => score.group === filter); + return data?.filter((score) => score.groups?.find((group) => group.name === filter)); } return data; }, [data, filter]); @@ -65,12 +66,16 @@ export default function Leaderboard({ token }: { token: string }) { filteredData?.map((score, key) => (
  • - {key + 1} + + {score.rank} +
    {score.pseudo} - {score.group} + + {score.groups?.map((g) => g.name).join(', ')} +
    diff --git a/ui/Puzzles.tsx b/ui/Puzzles.tsx index ce68e88..b3d8c3c 100644 --- a/ui/Puzzles.tsx +++ b/ui/Puzzles.tsx @@ -1,37 +1,89 @@ 'use client'; import { usePuzzles } from '@/lib/hooks/use-puzzles'; +import { type Chapter } from '@/lib/puzzles'; +import { cn } from '@/lib/utils'; import AppLink from './AppLink'; import Icon from './Icon'; export default function Puzzles({ token }: { token: string }) { const { data, isLoading } = usePuzzles({ token }); - console.log(data); + + // SOme chapters have a start date and a end date (for example, the first chapter is only available for 2 weeks), I want to want to lock the chapter if the current date is not between the start and end date + // I want to display a message to the user if the chapter is locked + + function isChapterLocked(chapter: Chapter) { + return ( + chapter.startDay && + chapter.endDay && + new Date() > new Date(chapter.startDay) && + new Date() < new Date(chapter.endDay) + ); + } + return ( <> {(!isLoading && - data?.chapters?.map((chapter) => ( + data?.map((chapter) => (

    - Chapitre {chapter.id} - {chapter.name} + Chapitre {chapter.id} - {chapter.name}{' '}

    -
      - {data?.puzzles.map((puzzle) => ( - -
    • - {puzzle.name} - -
    • -
      - ))} +
        + {chapter.puzzles && + chapter.puzzles.map((puzzle) => ( + +
      • tag.name.toLowerCase()) + .includes('easy'), + 'border-yellow-600/30': puzzle.tags + ?.map((tag) => tag.name.toLowerCase()) + .includes('medium'), + 'border-red-600/30': puzzle.tags + ?.map((tag) => tag.name.toLowerCase()) + .includes('hard'), + 'border-highlight-secondary/30': !puzzle.tags?.length + } + )} + > +
        + {puzzle.name} + {puzzle.tags?.length && ( +
        + {puzzle.tags + .filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name)) + .map((tag, i) => ( + + {tag.name} + + ))} +
        + )} +
        + +
      • +
        + ))}
    ))) || ( diff --git a/ui/Select.tsx b/ui/Select.tsx index 85f3275..da9a6eb 100644 --- a/ui/Select.tsx +++ b/ui/Select.tsx @@ -1,5 +1,4 @@ import { forwardRef } from 'react'; -import type { UseFormRegister } from 'react-hook-form'; import ErrorMessage from './ErrorMessage'; import Label from './Label'; @@ -17,7 +16,7 @@ const Select = forwardRef<