React auth provider
This commit is contained in:
parent
02956c9ef1
commit
fef736f547
21 changed files with 200 additions and 122 deletions
|
@ -1,2 +1,2 @@
|
||||||
NEXT_PUBLIC_API_URL=API
|
|
||||||
NEXT_PUBLIC_SITE_URL=SITE
|
NEXT_PUBLIC_SITE_URL=SITE
|
||||||
|
NEXT_PUBLIC_API_URL=API
|
|
@ -1,11 +1,11 @@
|
||||||
import Badge from '@/ui/Badge';
|
'use client';
|
||||||
|
|
||||||
export const metadata = {
|
import { UserContext } from '@/context/user';
|
||||||
title: 'Mes badges - Peer-at Code'
|
import Badge from '@/ui/Badge';
|
||||||
};
|
import { useContext } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
// TODO: Fetch badges from API and display them
|
const { data: me } = useContext(UserContext);
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col space-y-4">
|
<div className="flex h-full w-full flex-col space-y-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
@ -18,15 +18,19 @@ export default function Page() {
|
||||||
</header>
|
</header>
|
||||||
<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">
|
||||||
<Badge title="Je suis un teste" path="/assets/badges/java.png" alt="je suis un alt" />
|
{me?.badges ? (
|
||||||
<Badge title="Je suis un teste" path="/assets/badges/java.png" alt="je suis un alt" />
|
me?.badges.map((badge, i) => (
|
||||||
<Badge
|
<Badge
|
||||||
title="Peer-at What ?"
|
key={i}
|
||||||
path="/assets/badges/java.png"
|
name={badge.name}
|
||||||
type="hard"
|
src={badge.logo || '/assets/badges/java.png'}
|
||||||
alt="je suis un alt"
|
alt={badge.name}
|
||||||
earned
|
level={badge.level}
|
||||||
/>
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-muted">Aucun badge</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { UserProvider } from '@/context/user';
|
||||||
import Wrapper from '@/ui/dashboard/Wrapper';
|
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 (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col">
|
<div className="flex h-screen w-full flex-col">
|
||||||
<Wrapper>{children}</Wrapper>
|
<UserProvider token={token}>
|
||||||
|
<Wrapper>{children}</Wrapper>
|
||||||
|
</UserProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMe } from '@/lib/hooks/use-players';
|
import { UserContext } from '@/context/user';
|
||||||
import Card from '@/ui/Card';
|
import Card from '@/ui/Card';
|
||||||
import cookies from 'js-cookie';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const token = cookies.get('token');
|
const { data: me, isLoading } = useContext(UserContext);
|
||||||
const { data: me, isLoading } = useMe({ token: token! });
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col space-y-4">
|
<div className="flex h-full w-full flex-col space-y-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
@ -19,21 +18,16 @@ export default function Page() {
|
||||||
<Card
|
<Card
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
icon="pie-chart-line"
|
icon="pie-chart-line"
|
||||||
title="Puzzles"
|
title="Puzzles résolus"
|
||||||
data={me?.completions}
|
data={me?.completions}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
icon="award-line"
|
icon="award-line"
|
||||||
title="Badges"
|
title="Badges obtenus"
|
||||||
data={me?.badges || 'Aucun'}
|
data={me?.badges?.length || 'Aucun'}
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
isLoading={isLoading}
|
|
||||||
icon="bar-chart-line"
|
|
||||||
title="Score (classement plus tard)"
|
|
||||||
data={me?.score}
|
|
||||||
/>
|
/>
|
||||||
|
<Card isLoading={isLoading} icon="bar-chart-line" title="Rang actuel" data={me?.rank} />
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
20
context/user.tsx
Normal file
20
context/user.tsx
Normal file
|
@ -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 <UserContext.Provider value={{ data, isLoading, error }}>{children}</UserContext.Provider>;
|
||||||
|
};
|
|
@ -2,9 +2,7 @@ import useSWR from 'swr';
|
||||||
import { getPlayer } from '../players';
|
import { getPlayer } from '../players';
|
||||||
|
|
||||||
export function useMe({ token }: { token: string }) {
|
export function useMe({ token }: { token: string }) {
|
||||||
return useSWR('me', () => getPlayer({ token }), {
|
return useSWR('me', () => getPlayer({ token }));
|
||||||
revalidateOnReconnect: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlayer({ token, username }: { token: string; username: string }) {
|
export function usePlayer({ token, username }: { token: string; username: string }) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import fetcher from './fetcher';
|
import fetcher from './fetcher';
|
||||||
|
import type { Group } from './players';
|
||||||
|
|
||||||
export const getScores = async ({ token }: { token: string }): Promise<Score[]> => {
|
export const getScores = async ({ token }: { token: string }): Promise<Score[]> => {
|
||||||
const { data, status } = await fetcher.get(`/leaderboard`, {
|
const { data, status } = await fetcher.get(`/leaderboard`, {
|
||||||
|
@ -25,6 +26,7 @@ export type Score = {
|
||||||
tries: number;
|
tries: number;
|
||||||
completions: number;
|
completions: number;
|
||||||
pseudo: string;
|
pseudo: string;
|
||||||
group: string;
|
groups: Group[];
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
rank: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,6 +48,6 @@ export const navItems: NavItem[] = [
|
||||||
name: 'Paramètres',
|
name: 'Paramètres',
|
||||||
slug: 'settings',
|
slug: 'settings',
|
||||||
icon: 'equalizer-line',
|
icon: 'equalizer-line',
|
||||||
disabled: false
|
disabled: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -28,14 +28,26 @@ export const getPlayer = async ({
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
email: string;
|
email: string;
|
||||||
|
pseudo: string;
|
||||||
firstnames: string;
|
firstnames: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
description: string;
|
description: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
group: string;
|
groups: Group[];
|
||||||
score: number;
|
score: number;
|
||||||
tries: number;
|
tries: number;
|
||||||
completions: number;
|
completions: number;
|
||||||
pseudo: string;
|
rank: number;
|
||||||
badges: any[];
|
badges: Badge[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Badge = {
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
logo?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Group = {
|
||||||
|
name: string;
|
||||||
|
chapter?: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ export const getChapters = async ({ token }: { token: string }): Promise<Chapter
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let chapters = data;
|
const chapters = data;
|
||||||
|
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
throw new Error('Failed to fetch puzzles');
|
throw new Error('Failed to fetch puzzles');
|
||||||
|
@ -17,8 +17,6 @@ export const getChapters = async ({ token }: { token: string }): Promise<Chapter
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
chapters = chapters.filter((chapter: Chapter) => chapter.id !== 0);
|
|
||||||
|
|
||||||
return chapters as Chapter[];
|
return chapters as Chapter[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,24 +46,17 @@ export const getChapter = async ({
|
||||||
return chapter as Chapter;
|
return chapter as Chapter;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPuzzles = async ({
|
export const getPuzzles = async ({ token }: { token: string }): Promise<Chapter[]> => {
|
||||||
token
|
|
||||||
}: {
|
|
||||||
token: string;
|
|
||||||
}): Promise<{ chapters: Chapter[]; puzzles: Puzzle[] }> => {
|
|
||||||
const chapters = await getChapters({ token });
|
const chapters = await getChapters({ token });
|
||||||
const puzzles: Puzzle[] = [];
|
|
||||||
|
|
||||||
for (const chapter of chapters) {
|
for (let i = 0; i < chapters.length; i++) {
|
||||||
const puzzlesByChapter = await getChapter({ token, id: chapter.id });
|
const chapter = chapters[i];
|
||||||
if (!puzzlesByChapter?.puzzles) continue;
|
const chapterData = await getChapter({ token, id: chapter.id });
|
||||||
puzzles.push(...puzzlesByChapter!.puzzles);
|
if (!chapterData) continue;
|
||||||
|
chapters[i].puzzles = chapterData.puzzles;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return chapters as Chapter[];
|
||||||
chapters: chapters as Chapter[],
|
|
||||||
puzzles: puzzles as Puzzle[]
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPuzzle = async ({ token, id }: { token: string; id: number }): Promise<Puzzle> => {
|
export const getPuzzle = async ({ token, id }: { token: string; id: number }): Promise<Puzzle> => {
|
||||||
|
@ -92,10 +83,17 @@ export type Puzzle = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
tags: Tag[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Chapter = {
|
export type Chapter = {
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
|
name: string;
|
||||||
puzzles: Puzzle[];
|
puzzles: Puzzle[];
|
||||||
|
startDay?: string;
|
||||||
|
endDay?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Tag = {
|
||||||
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,10 +9,6 @@ import { getURL } from './lib/utils';
|
||||||
export async function middleware(req: NextRequest) {
|
export async function middleware(req: NextRequest) {
|
||||||
const res = NextResponse.next();
|
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;
|
const token = req.cookies.get('token')?.value;
|
||||||
|
|
||||||
if (req.nextUrl.pathname.includes('dashboard') && !token)
|
if (req.nextUrl.pathname.includes('dashboard') && !token)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "peer-at-code",
|
"name": "peer-at-code",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 1.2 MiB |
|
@ -1,3 +1,4 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
37
ui/Badge.tsx
37
ui/Badge.tsx
|
@ -2,42 +2,37 @@ import Image from 'next/image';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export type Difficulty = 'easy' | 'medium' | 'hard';
|
export const DIFFICULTY_COLOR = {
|
||||||
|
1: 'green',
|
||||||
export const DIFFICULTY = {
|
2: 'yellow',
|
||||||
1: 'easy',
|
3: 'red'
|
||||||
2: 'medium',
|
};
|
||||||
3: 'hard'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Badge({
|
export default function Badge({
|
||||||
title,
|
name,
|
||||||
path,
|
src,
|
||||||
alt,
|
alt,
|
||||||
type = 'easy',
|
level
|
||||||
earned = false
|
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
name: string;
|
||||||
path: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
type?: Difficulty;
|
level: number;
|
||||||
earned?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-24 flex-col space-y-2 text-center">
|
<div className="flex w-24 flex-col space-y-2 text-center">
|
||||||
<Image
|
<Image
|
||||||
src={path}
|
src={`data:image;base64,${src}`}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className={cn(`rounded-full border-2 lg:border-4`, {
|
className={cn(`rounded-full border-2 lg:border-4`, {
|
||||||
'border-green-600': type === 'easy',
|
'border-green-600': level === 1,
|
||||||
'border-yellow-600': type === 'medium',
|
'border-yellow-600': level === 2,
|
||||||
'border-red-600': type === 'hard',
|
'border-red-600': level === 3
|
||||||
'border-gray-600 opacity-40': !earned
|
|
||||||
})}
|
})}
|
||||||
width={500}
|
width={500}
|
||||||
height={500}
|
height={500}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-semibold">{earned ? title : '****'}</span>
|
<span className="text-sm font-semibold">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import ErrorMessage from './ErrorMessage';
|
import ErrorMessage from './ErrorMessage';
|
||||||
import Icon from './Icon';
|
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
const Input = forwardRef<
|
const Input = forwardRef<
|
||||||
|
|
|
@ -6,22 +6,23 @@ import { useMemo, useState } from 'react';
|
||||||
import AvatarComponent from './Avatar';
|
import AvatarComponent from './Avatar';
|
||||||
import Select from './Select';
|
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 }) {
|
export default function Leaderboard({ token }: { token: string }) {
|
||||||
const { data, isLoading } = useLeaderboard({ token });
|
const { data, isLoading } = useLeaderboard({ token });
|
||||||
|
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
let options;
|
let options = [] as { value: string; title: string }[];
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
options = data
|
options = data
|
||||||
.filter((score, index, self) => {
|
.filter((score) => score.groups && score.groups.length > 0)
|
||||||
return index === self.findIndex((t) => t.group === score.group) && score.group !== '';
|
.map((score) => score.groups.map((group) => ({ value: group.name, title: group.name })))
|
||||||
})
|
.flat()
|
||||||
.sort((a, b) => (a.group > b.group ? 1 : -1))
|
.filter((group, index, self) => self.findIndex((g) => g.value === group.value) === index)
|
||||||
.map((score) => ({ value: score.group, title: score.group }));
|
.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
options.unshift({ value: '', title: 'Tous' });
|
options.unshift({ value: '', title: 'Tous' });
|
||||||
options.push({ value: 'no-group', title: 'Sans groupe' });
|
options.push({ value: 'no-group', title: 'Sans groupe' });
|
||||||
}
|
}
|
||||||
|
@ -29,9 +30,9 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (filter === 'no-group') {
|
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;
|
return data;
|
||||||
}, [data, filter]);
|
}, [data, filter]);
|
||||||
|
@ -65,12 +66,16 @@ export default function Leaderboard({ token }: { token: string }) {
|
||||||
filteredData?.map((score, key) => (
|
filteredData?.map((score, key) => (
|
||||||
<li key={key} className="flex justify-between space-x-2">
|
<li key={key} className="flex justify-between space-x-2">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className={cn('font-semibold', scoreColors[key])}>{key + 1}</span>
|
<span className={cn('font-semibold', SCORE_COLORS[score.rank - 1])}>
|
||||||
|
{score.rank}
|
||||||
|
</span>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<AvatarComponent name={score.pseudo} src={score.avatar} className="h-9 w-9" />
|
<AvatarComponent name={score.pseudo} src={score.avatar} className="h-9 w-9" />
|
||||||
<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">{score.pseudo}</span>
|
<span className="text-lg">{score.pseudo}</span>
|
||||||
<span className="text-sm text-muted">{score.group}</span>
|
<span className="text-sm text-muted">
|
||||||
|
{score.groups?.map((g) => g.name).join(', ')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,37 +1,89 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
||||||
|
import { type Chapter } from '@/lib/puzzles';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import AppLink from './AppLink';
|
import AppLink from './AppLink';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
|
|
||||||
export default function Puzzles({ token }: { token: string }) {
|
export default function Puzzles({ token }: { token: string }) {
|
||||||
const { data, isLoading } = usePuzzles({ token });
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{(!isLoading &&
|
{(!isLoading &&
|
||||||
data?.chapters?.map((chapter) => (
|
data?.map((chapter) => (
|
||||||
<div key={chapter.id} className="flex flex-col space-y-4">
|
<div key={chapter.id} className="flex flex-col space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-xl font-semibold sm:text-2xl">
|
<h3 className="text-xl font-semibold sm:text-2xl">
|
||||||
Chapitre {chapter.id} - {chapter.name}
|
Chapitre {chapter.id} - {chapter.name}{' '}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-1 w-1/4 rounded-lg bg-gray-200">
|
<div className="h-1 w-1/4 rounded-lg bg-gray-200">
|
||||||
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
|
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="flex flex-col space-y-4">
|
<ul
|
||||||
{data?.puzzles.map((puzzle) => (
|
className={cn('flex flex-col space-y-4', {
|
||||||
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}>
|
// If the chapter is locked i want to add a class to li children to make them unclickable
|
||||||
<li className="group flex items-center justify-between rounded-md bg-primary-700 p-4 font-code hover:bg-primary-600">
|
'pointer-events-none': isChapterLocked(chapter)
|
||||||
<span className="text-base font-semibold">{puzzle.name}</span>
|
})}
|
||||||
<Icon
|
>
|
||||||
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
|
{chapter.puzzles &&
|
||||||
name="arrow-right-line"
|
chapter.puzzles.map((puzzle) => (
|
||||||
/>
|
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}>
|
||||||
</li>
|
<li
|
||||||
</AppLink>
|
className={cn(
|
||||||
))}
|
'group flex items-center justify-between rounded-md border-2 bg-primary-700 p-4 font-code hover:bg-primary-600',
|
||||||
|
{
|
||||||
|
'border-green-600/30': puzzle.tags
|
||||||
|
?.map((tag) => 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
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<span className="text-base font-semibold">{puzzle.name}</span>
|
||||||
|
{puzzle.tags?.length && (
|
||||||
|
<div className="flex gap-x-2 text-sm text-muted">
|
||||||
|
{puzzle.tags
|
||||||
|
.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name))
|
||||||
|
.map((tag, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={cn('inline-block rounded-md bg-primary-900 px-2 py-1')}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
|
||||||
|
name="arrow-right-line"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</AppLink>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))) || (
|
))) || (
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import type { UseFormRegister } from 'react-hook-form';
|
|
||||||
import ErrorMessage from './ErrorMessage';
|
import ErrorMessage from './ErrorMessage';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ const Select = forwardRef<
|
||||||
<Label label={label} description={description} required={props.required} className={className}>
|
<Label label={label} description={description} required={props.required} className={className}>
|
||||||
<select
|
<select
|
||||||
className={
|
className={
|
||||||
'w-full cursor-pointer overflow-hidden rounded-lg border-2 border-highlight-primary bg-transparent px-5 py-2.5 text-sm font-medium text-secondary opacity-80 outline-none transition-opacity hover:opacity-100 disabled:opacity-50'
|
'w-full cursor-pointer overflow-hidden rounded-lg border-2 border-highlight-primary bg-transparent px-5 py-2.5 text-sm font-medium text-secondary opacity-80 outline-none outline-0 transition-opacity hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50'
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { NavItem, navItems } from '@/lib/nav-items';
|
import { NavItem, navItems } from '@/lib/nav-items';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||||
import AppLink from '../AppLink';
|
import AppLink from '../AppLink';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
|
@ -18,12 +19,12 @@ export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex p-6">
|
<div className="flex w-full justify-center p-[9px]">
|
||||||
<AppLink className="truncate" href="/">
|
<AppLink href="/">
|
||||||
<h1>Peer-at Code</h1>
|
<Image src="/assets/brand/peerat.png" alt="Peer-at" width={50} height={50} />
|
||||||
</AppLink>
|
</AppLink>
|
||||||
</div>
|
</div>
|
||||||
<div className=" px-4 ">
|
<div className="px-4">
|
||||||
<hr className="border-highlight-primary" />
|
<hr className="border-highlight-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 pt-4">
|
<div className="px-4 pt-4">
|
||||||
|
@ -76,7 +77,7 @@ function NavItem({
|
||||||
className={cn('flex justify-center rounded-md px-3 py-3 text-sm lg:justify-start', {
|
className={cn('flex justify-center rounded-md px-3 py-3 text-sm lg:justify-start', {
|
||||||
'text-muted hover:text-secondary': !isActive,
|
'text-muted hover:text-secondary': !isActive,
|
||||||
'bg-highlight-primary text-secondary': isActive,
|
'bg-highlight-primary text-secondary': isActive,
|
||||||
'text-gray-600 hover:text-gray-600': item.disabled,
|
'cursor-not-allowed text-gray-600 hover:text-gray-600': item.disabled,
|
||||||
'justify-center lg:justify-start': isOpen,
|
'justify-center lg:justify-start': isOpen,
|
||||||
'justify-start sm:justify-center': !isOpen
|
'justify-start sm:justify-center': !isOpen
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMe } from '@/lib/hooks/use-players';
|
import { UserContext } from '@/context/user';
|
||||||
import { titleCase } from '@/lib/utils';
|
import { titleCase } from '@/lib/utils';
|
||||||
import cookies from 'js-cookie';
|
import cookies from 'js-cookie';
|
||||||
import { useRouter, useSelectedLayoutSegment } from 'next/navigation';
|
import { useRouter, useSelectedLayoutSegment } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import AvatarComponent from '../Avatar';
|
import AvatarComponent from '../Avatar';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
import Popover from '../Popover';
|
import Popover from '../Popover';
|
||||||
|
@ -14,9 +14,7 @@ export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: (
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segment = useSelectedLayoutSegment();
|
const segment = useSelectedLayoutSegment();
|
||||||
|
|
||||||
const token = cookies.get('token');
|
const { data: me, isLoading } = useContext(UserContext);
|
||||||
|
|
||||||
const { data: me, isLoading } = useMe({ token: token! });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue