Merge pull request #5 from Peer-at-Code/feat/data-fetching
Added basic data fetch & ui update
This commit is contained in:
commit
79dc7eef07
22 changed files with 579 additions and 103 deletions
|
@ -1,16 +1,11 @@
|
|||
import { type ReactNode } from 'react';
|
||||
|
||||
import Sidenav from '@/ui/dashboard/Sidenav';
|
||||
import Wrapper from '@/ui/dashboard/Wrapper';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="flex flex-1 flex-col overflow-hidden sm:flex-row">
|
||||
<Sidenav />
|
||||
<div className="mx-4 flex flex-1 transform flex-col pt-4 pb-8 duration-300 ease-in-out sm:mx-auto sm:py-8 md:max-w-6xl">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<Wrapper>{children}</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
16
app/dashboard/puzzles/[id]/page.tsx
Normal file
16
app/dashboard/puzzles/[id]/page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Puzzle from '@/ui/Puzzle';
|
||||
|
||||
export default async function Page({ params }: { params: { id: string } }) {
|
||||
const { id } = params;
|
||||
|
||||
return (
|
||||
// <SWRFallback fallback={{ [`puzzles/${id}`]: puzzle }}>
|
||||
<Puzzle id={id} />
|
||||
// </SWRFallback>
|
||||
);
|
||||
}
|
||||
|
||||
// export async function generateStaticParams() {
|
||||
// const { puzzles } = await getPuzzles();
|
||||
// return puzzles.map(({ id }) => ({ params: { id } }));
|
||||
// }
|
File diff suppressed because one or more lines are too long
BIN
app/fonts/Karrik-Regular.woff2
Normal file
BIN
app/fonts/Karrik-Regular.woff2
Normal file
Binary file not shown.
BIN
app/fonts/Typefesse_Claire-Obscure.woff2
Normal file
BIN
app/fonts/Typefesse_Claire-Obscure.woff2
Normal file
Binary file not shown.
BIN
app/fonts/Typefesse_Pleine.woff2
Normal file
BIN
app/fonts/Typefesse_Pleine.woff2
Normal file
Binary file not shown.
|
@ -1,12 +1,34 @@
|
|||
import '@/styles/globals.css';
|
||||
import 'remixicon/fonts/remixicon.css';
|
||||
|
||||
import { Fira_Code } from '@next/font/google';
|
||||
import localFont from '@next/font/local';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
const sans = localFont({
|
||||
variable: '--font-sans',
|
||||
src: './fonts/Karrik-Regular.woff2',
|
||||
weight: 'variable'
|
||||
});
|
||||
|
||||
const code = Fira_Code({
|
||||
variable: '--font-code',
|
||||
subsets: ['latin'],
|
||||
weight: 'variable'
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="fr" dir="ltr" className={cn('scroll-smooth bg-light-dark [color-scheme:dark]')}>
|
||||
<html
|
||||
lang="fr"
|
||||
dir="ltr"
|
||||
className={cn(
|
||||
'scroll-smooth bg-gradient-to-b from-primary-800 to-primary-900 [color-scheme:dark]',
|
||||
sans.variable,
|
||||
code.variable
|
||||
)}
|
||||
>
|
||||
<head />
|
||||
<body className="relative min-h-screen">
|
||||
<main>{children}</main>
|
||||
|
|
14
lib/hooks/use-puzzles.ts
Normal file
14
lib/hooks/use-puzzles.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import useSWR from 'swr';
|
||||
import { getChapters, getPuzzle, getPuzzles } from '../puzzles';
|
||||
|
||||
export function useChapters() {
|
||||
return useSWR('chapters', () => getChapters());
|
||||
}
|
||||
|
||||
export function usePuzzles() {
|
||||
return useSWR('puzzles', () => getPuzzles());
|
||||
}
|
||||
|
||||
export function usePuzzle(id: string) {
|
||||
return useSWR(`puzzles/${id}`, () => getPuzzle(id));
|
||||
}
|
74
lib/puzzles.ts
Normal file
74
lib/puzzles.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
export const getChapters = async (): Promise<Chapter[]> => {
|
||||
const req = await fetch(`http://170.75.166.204/chapters`);
|
||||
|
||||
const chapters = await req.json();
|
||||
|
||||
if (!req.ok) {
|
||||
throw new Error('Failed to fetch puzzles');
|
||||
}
|
||||
|
||||
if (!chapters) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return chapters as Chapter[];
|
||||
};
|
||||
|
||||
export const getPuzzlesByChapter = async (chapitre: string): Promise<Puzzle[]> => {
|
||||
const req = await fetch(`http://170.75.166.204/chapter/${chapitre}`);
|
||||
|
||||
const { puzzles } = await req.json();
|
||||
|
||||
if (!req.ok) {
|
||||
throw new Error('Failed to fetch puzzles');
|
||||
}
|
||||
|
||||
if (!puzzles) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return puzzles as Puzzle[];
|
||||
};
|
||||
|
||||
export const getPuzzles = async (): Promise<{ chapters: Chapter[]; puzzles: Puzzle[] }> => {
|
||||
const chapters = await getChapters();
|
||||
let puzzles: Puzzle[] = [];
|
||||
|
||||
for (const chapter of chapters) {
|
||||
const puzzlesByChapter = await getPuzzlesByChapter(chapter.id);
|
||||
puzzles = [...puzzles, ...puzzlesByChapter];
|
||||
}
|
||||
|
||||
return {
|
||||
chapters: chapters as Chapter[],
|
||||
puzzles: puzzles as Puzzle[]
|
||||
};
|
||||
};
|
||||
|
||||
export const getPuzzle = async (id: string): Promise<Puzzle> => {
|
||||
const req = await fetch(`http://170.75.166.204/puzzle/${id}`);
|
||||
|
||||
const puzzle = await req.json();
|
||||
|
||||
if (!req.ok) {
|
||||
throw new Error('Failed to fetch puzzle');
|
||||
}
|
||||
|
||||
if (!puzzle) {
|
||||
return {} as Puzzle;
|
||||
}
|
||||
|
||||
return puzzle as Puzzle;
|
||||
};
|
||||
|
||||
export type Puzzle = {
|
||||
chapter: string;
|
||||
name: string;
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type Chapter = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
experimental: {
|
||||
appDir: true
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"next": "13.1.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.1",
|
||||
"remixicon": "^2.5.0",
|
||||
"swr": "^2.0.3",
|
||||
"tailwind-merge": "^1.9.0",
|
||||
|
|
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
|
@ -20,6 +20,7 @@ specifiers:
|
|||
prettier-plugin-tailwindcss: ^0.2.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0
|
||||
react-hook-form: ^7.43.1
|
||||
remixicon: ^2.5.0
|
||||
swr: ^2.0.3
|
||||
tailwind-merge: ^1.9.0
|
||||
|
@ -33,6 +34,7 @@ dependencies:
|
|||
next: 13.1.6_biqbaboplfbrettd7655fr4n2y
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-hook-form: 7.43.1_react@18.2.0
|
||||
remixicon: 2.5.0
|
||||
swr: 2.0.3_react@18.2.0
|
||||
tailwind-merge: 1.9.0
|
||||
|
@ -2268,6 +2270,15 @@ packages:
|
|||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-hook-form/7.43.1_react@18.2.0:
|
||||
resolution: {integrity: sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-is/16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: true
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
|
@ -7,9 +9,84 @@ module.exports = {
|
|||
},
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans],
|
||||
code: ['var(--font-code)', ...defaultTheme.fontFamily.serif]
|
||||
},
|
||||
colors: {
|
||||
'light-dark': '#282828',
|
||||
dark: '#202020'
|
||||
...require('tailwindcss/colors'),
|
||||
primary: {
|
||||
900: 'hsl(258deg 15% 7%)',
|
||||
800: 'hsl(258deg 15% 11%)',
|
||||
700: 'hsl(258deg 15% 15%)',
|
||||
600: 'hsl(258deg 15% 20%)',
|
||||
500: 'hsl(258deg 15% 25%)',
|
||||
400: 'hsl(258deg 14% 35%)',
|
||||
300: 'hsl(258deg 13% 45%)',
|
||||
200: 'hsl(258deg 13% 55%)',
|
||||
100: 'hsl(258deg 10% 65%)',
|
||||
50: 'hsl(258deg 8% 85%)',
|
||||
0: 'hsl(258deg 8% 100%)'
|
||||
},
|
||||
brand: {
|
||||
DEFAULT: '#1c56cb',
|
||||
accent: '#236bfe'
|
||||
},
|
||||
success: {
|
||||
DEFAULT: 'hsl(104deg 39% 59%)',
|
||||
secondary: '#b5e4ca',
|
||||
background: '#60a747'
|
||||
},
|
||||
info: {
|
||||
DEFAULT: 'hsl(258deg 78% 77%)',
|
||||
secondary: '#ccb8f9',
|
||||
background: '#9878de'
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: 'hsl(39deg 100% 67%)',
|
||||
secondary: '#ffd88d',
|
||||
background: '#da9b34'
|
||||
},
|
||||
error: {
|
||||
DEFAULT: 'hsl(7deg 100% 67%)',
|
||||
secondary: '#ffbc99',
|
||||
background: '#cd4634'
|
||||
},
|
||||
highlight: {
|
||||
primary: 'hsl(258deg 15% 17%)',
|
||||
secondary: 'hsl(258deg 10% 46%)'
|
||||
},
|
||||
product: {
|
||||
ignite: 'hsl(8deg 89% 57%)',
|
||||
pipe: 'hsl(214deg 100% 58%)',
|
||||
channels: 'hsl(46deg 74% 51%)'
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
primary: {
|
||||
DEFAULT: 'hsl(258deg 15% 7%)',
|
||||
900: 'hsl(258deg 15% 7%)',
|
||||
800: 'hsl(258deg 15% 11%)',
|
||||
700: 'hsl(258deg 15% 15%)',
|
||||
600: 'hsl(258deg 15% 20%)',
|
||||
500: 'hsl(258deg 15% 25%)',
|
||||
400: 'hsl(258deg 14% 35%)',
|
||||
300: 'hsl(258deg 13% 45%)',
|
||||
200: 'hsl(258deg 13% 55%)',
|
||||
100: 'hsl(258deg 10% 65%)',
|
||||
50: 'hsl(258deg 8% 85%)',
|
||||
0: 'hsl(258deg 8% 100%)'
|
||||
},
|
||||
secondary: 'hsl(258deg 15% 11%)',
|
||||
tertiary: 'hsl(258deg 15% 17%)',
|
||||
contrast: '#4f5450'
|
||||
},
|
||||
textColor: {
|
||||
primary: 'hsl(258deg 8% 100%)',
|
||||
secondary: 'hsl(258deg 8% 84%)',
|
||||
tertiary: 'hsl(258deg 8% 65%)',
|
||||
secondaryAccent: '#e2e8f0',
|
||||
muted: 'hsl(258deg 7% 46%)'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,16 +4,17 @@ import { forwardRef } from 'react';
|
|||
const Button = forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
kind?: 'default' | 'outline' | 'danger';
|
||||
kind?: 'default' | 'danger' | 'brand';
|
||||
}
|
||||
>(({ kind = 'default', className, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-lg border-0 px-5 py-2.5 text-center text-sm font-medium outline-none transition-colors focus:outline-none focus:ring-0 disabled:opacity-50',
|
||||
'inline-flex items-center justify-center rounded-md border-0 px-5 py-2.5 text-center text-sm font-medium outline-none transition-colors focus:outline-none focus:ring-0 disabled:opacity-50',
|
||||
{
|
||||
'bg-[#424242] hover:bg-[#424242]/60': kind === 'default',
|
||||
'bg-red-600 hover:bg-red-600/60': kind === 'danger'
|
||||
'bg-highlight-primary hover:bg-highlight-primary/60': kind === 'default',
|
||||
'bg-error hover:bg-error/60': kind === 'danger',
|
||||
'bg-brand hover:bg-brand/60': kind === 'brand'
|
||||
},
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -14,7 +14,7 @@ const Input = forwardRef<
|
|||
<Label label={label} description={description} required={props.required} className={className}>
|
||||
<input
|
||||
ref={ref}
|
||||
className="w-full rounded-lg border-0 bg-[#424242] px-5 py-2.5 text-sm font-medium outline-none transition-colors focus:outline-none focus:ring-0 disabled:opacity-50"
|
||||
className="w-full rounded-md border-0 bg-highlight-primary px-5 py-2.5 text-sm font-medium ring-offset-0 focus:ring-brand disabled:opacity-50"
|
||||
{...props}
|
||||
/>
|
||||
</Label>
|
||||
|
|
52
ui/Puzzle.tsx
Normal file
52
ui/Puzzle.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { usePuzzle } from '@/lib/hooks/use-puzzles';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Button from './Button';
|
||||
import Input from './Input';
|
||||
import ToHTML from './ToHTML';
|
||||
|
||||
export default function Puzzle({ id }: { id: string }) {
|
||||
const { data: puzzle, isLoading } = usePuzzle(id);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!puzzle) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-between space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h2 className="text-4xl font-bold">{puzzle.name}</h2>
|
||||
<p className="text-sm text-muted">Chapitre {puzzle.chapter}</p>
|
||||
</div>
|
||||
<div className="flex h-screen overflow-y-auto">
|
||||
<ToHTML className="font-code" html={puzzle.content} />
|
||||
</div>
|
||||
<form className="flex w-full flex-col justify-between sm:flex-row">
|
||||
<div className="flex flex-col space-x-0 sm:flex-row sm:space-x-6">
|
||||
<Input
|
||||
className="w-full sm:w-1/3"
|
||||
label="Réponse"
|
||||
name="answer"
|
||||
type="text"
|
||||
placeholder="12"
|
||||
/>
|
||||
<Input
|
||||
className="h-16 w-full sm:w-1/3"
|
||||
label="Code"
|
||||
name="code_file"
|
||||
type="file"
|
||||
accept=".py,.js,.ts,.java,.rust,.c"
|
||||
/>
|
||||
</div>
|
||||
<Button kind="brand" className="mt-6" type="submit">
|
||||
Envoyer
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
42
ui/Puzzles.tsx
Normal file
42
ui/Puzzles.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use client';
|
||||
|
||||
import { usePuzzles } from '@/lib/hooks/use-puzzles';
|
||||
import AppLink from './AppLink';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default function Puzzles() {
|
||||
const { data, isLoading } = usePuzzles();
|
||||
return (
|
||||
<>
|
||||
{!isLoading &&
|
||||
data?.chapters?.map((chapter) => (
|
||||
<div key={chapter.id} className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
Chapitre {chapter.id} - {chapter.name}
|
||||
</h3>
|
||||
<div className="h-1 w-1/4 bg-gray-200">
|
||||
<div className="h-1 w-1/2 bg-brand" />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="flex flex-col space-y-4">
|
||||
{data?.puzzles.map((puzzle) => (
|
||||
<AppLink key={puzzle.id} href={`/dashboard/puzzles/${puzzle.id}`}>
|
||||
<li className="group flex justify-between rounded-md bg-primary-700 p-4 font-code hover:bg-primary-600">
|
||||
<div className="flex space-x-4">
|
||||
<span className="">{puzzle.id}</span>
|
||||
<span className="font-semibold">{puzzle.name}</span>
|
||||
</div>
|
||||
<Icon
|
||||
className="-translate-x-2 transform-gpu duration-300 group-hover:translate-x-0"
|
||||
name="arrow-right-line"
|
||||
/>
|
||||
</li>
|
||||
</AppLink>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
15
ui/SWRFallback.tsx
Normal file
15
ui/SWRFallback.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
export default function SWRFallback({
|
||||
children,
|
||||
fallback
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
fallback: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}) {
|
||||
return <SWRConfig value={{ fallback, keepPreviousData: true }}>{children}</SWRConfig>;
|
||||
}
|
119
ui/UserAuthForm.tsx
Normal file
119
ui/UserAuthForm.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
'use client';
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import AppLink from './AppLink';
|
||||
import Button from './Button';
|
||||
import Input from './Input';
|
||||
|
||||
export default function UserAuthForm() {
|
||||
return (
|
||||
<>
|
||||
<AuthForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
email?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
function AuthForm() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
email: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname()!;
|
||||
const isSignIn = pathname.includes('sign-in');
|
||||
|
||||
async function onSubmit(data: FormData) {
|
||||
const res = await fetch(`http://170.75.166.204/${isSignIn ? 'login' : 'register'}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data
|
||||
})
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="bg-dark flex flex-col space-y-4 rounded-md p-6"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
{!isSignIn && (
|
||||
<Input
|
||||
label="Adresse e-mail"
|
||||
type="email"
|
||||
placeholder="Ex: peer-at@exemple.be"
|
||||
required
|
||||
error={
|
||||
errors.email?.message
|
||||
// &&
|
||||
// (isSignIn ? (
|
||||
// <>
|
||||
// {translations.noAccountAssociated}{' '}
|
||||
// <AppLink className="underline" href="/sign-up">
|
||||
// {translations.signUpQuestion}
|
||||
// </AppLink>
|
||||
// </>
|
||||
// ) : (
|
||||
// errors.email.message
|
||||
// ))
|
||||
}
|
||||
{...register('email')}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="Nom d'utilisateur"
|
||||
type="text"
|
||||
placeholder='Ex: "PeerAt"'
|
||||
required
|
||||
{...register('username', { required: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
placeholder='Ex: "MotDePasse123"'
|
||||
required
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
<Button type="submit" kind="brand">
|
||||
Se connecter
|
||||
</Button>
|
||||
{/* {!isSignIn && (
|
||||
<p className="items-center text-sm text-gray-400">
|
||||
En cliquant sur continuer, vous acceptez les{' '}
|
||||
<AppLink className="text-white underline" href="/privacy-policy" target="_blank">
|
||||
Politique de confidentialité
|
||||
</AppLink>
|
||||
.
|
||||
</p>
|
||||
)} */}
|
||||
<p className="flex flex-col items-center text-sm text-gray-400">
|
||||
{isSignIn ? "Vous n'avez pas de compte?" : 'Vous possédez un compte?'}{' '}
|
||||
<AppLink className="text-white underline" href={isSignIn ? '/sign-up' : '/sign-in'}>
|
||||
{isSignIn ? "S'inscrire maintenant" : 'Se connecter'}
|
||||
</AppLink>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -2,98 +2,92 @@
|
|||
|
||||
import { NavItem, navItems } from '@/lib/nav-items';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Image from 'next/image';
|
||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import Logo from '../../public/logo.webp';
|
||||
import AppLink from '../AppLink';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export default function Sidenav() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
function toggleSidenav() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'relative flex h-24 flex-row justify-between bg-dark shadow-md transition-all duration-300 ease-in-out sm:h-screen sm:flex-col',
|
||||
'absolute z-10 h-screen w-28 border-r border-highlight-primary bg-gradient-to-b from-primary-800 to-primary-900 shadow-md transition-all duration-300 ease-in-out sm:relative sm:flex sm:flex-col md:w-60',
|
||||
{
|
||||
'sm:w-60': isOpen,
|
||||
'w-full sm:w-28': !isOpen
|
||||
'bottom-0 -translate-x-full sm:translate-x-0': !isOpen,
|
||||
'bottom-0 w-full sm:w-28': isOpen
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row sm:flex-col">
|
||||
<div className="flex px-4 pt-0 sm:block sm:items-center sm:pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<AppLink href="/" className="hidden sm:block">
|
||||
<Image src={Logo} alt="Peer-at Code Logo" className=" h-10 w-10" />
|
||||
</AppLink>
|
||||
<button
|
||||
className="flex items-center justify-center rounded bg-light-dark p-1"
|
||||
onClick={toggleSidenav}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left-line"
|
||||
className={cn('transition duration-300', {
|
||||
'rotate-0': isOpen,
|
||||
'rotate-180': !isOpen
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex p-6">
|
||||
<AppLink className="truncate" href="/">
|
||||
<h1>Peer-at Code</h1>
|
||||
</AppLink>
|
||||
</div>
|
||||
<div className="hidden px-4 pt-4 sm:block">
|
||||
<hr className="border-light-dark" />
|
||||
<div className=" px-4 ">
|
||||
<hr className="border-highlight-primary" />
|
||||
</div>
|
||||
<div className="hidden px-4 pt-4 sm:block">
|
||||
<div className="px-4 pt-4">
|
||||
<ul className="space-y-4">
|
||||
{navItems.map((item) => (
|
||||
<li key={item.slug}>
|
||||
<NavItem item={item} isOpen={isOpen} />
|
||||
<NavItem item={item} isOpen={isOpen} onClick={toggle} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row sm:flex-col">
|
||||
<div className="px-4 py-4">
|
||||
<button className="flex w-full items-center space-x-2 truncate rounded bg-light-dark p-3">
|
||||
<Icon className="text-2xl" name="user-line" />
|
||||
<span className="truncate">Hacktiviste</span>
|
||||
</button>
|
||||
<div className="px-4 pt-4">
|
||||
<hr className="border-highlight-primary" />
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<NavItem
|
||||
item={{
|
||||
name: 'Tutoriels',
|
||||
slug: '/dashboard/tutorials',
|
||||
icon: 'question-line',
|
||||
disabled: false
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
onClick={toggle}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ item, isOpen }: { item: NavItem; isOpen: boolean }) {
|
||||
function NavItem({
|
||||
item,
|
||||
isOpen,
|
||||
onClick
|
||||
}: {
|
||||
item: NavItem;
|
||||
isOpen: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const segment = useSelectedLayoutSegment();
|
||||
const isActive = segment?.split('/').pop() === item.slug || (item.slug === '' && !segment);
|
||||
return (
|
||||
<AppLink
|
||||
href={item.disabled ? '/dashboard' : `/dashboard/${item.slug}`}
|
||||
className={cn('flex rounded-md px-3 py-3 text-sm font-medium', {
|
||||
'bg-light-dark text-gray-400 hover:bg-light-dark/60 hover:text-white': !isActive,
|
||||
'bg-blue-500 text-white': isActive,
|
||||
className={cn('flex justify-center rounded-md px-3 py-3 text-sm md:justify-start', {
|
||||
'text-muted hover:text-white': !isActive,
|
||||
'bg-highlight-primary text-secondary': isActive,
|
||||
'text-gray-600 hover:text-gray-600': item.disabled,
|
||||
'justify-start': isOpen,
|
||||
'justify-center': !isOpen
|
||||
'justify-center md:justify-start': isOpen,
|
||||
'justify-start sm:justify-center': !isOpen
|
||||
})}
|
||||
onClick={onClick}
|
||||
passHref
|
||||
>
|
||||
<div
|
||||
className={cn('flex items-center', {
|
||||
'space-x-2': isOpen
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="text-2xl" name={item.icon} />
|
||||
<span
|
||||
className={cn({
|
||||
className={cn('hidden md:block', {
|
||||
'block sm:hidden': isOpen,
|
||||
hidden: !isOpen
|
||||
})}
|
||||
>
|
||||
|
|
42
ui/dashboard/Usernav.tsx
Normal file
42
ui/dashboard/Usernav.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use client';
|
||||
|
||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export default function Usernav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||
const segment = useSelectedLayoutSegment();
|
||||
|
||||
// segment to TitleCase
|
||||
const titleCase = (str: string) => {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-50 flex flex-row items-center justify-between border-b border-solid border-highlight-primary bg-secondary py-4 px-8">
|
||||
<div className="flex flex-row items-center space-x-2 sm:space-x-0">
|
||||
<div className="flex items-center">
|
||||
<button onClick={toggle} className="block sm:hidden">
|
||||
{isOpen ? <Icon name="close-line" /> : <Icon name="menu-2-line" />}
|
||||
</button>
|
||||
</div>
|
||||
{segment && (
|
||||
<div className="flex items-center justify-center text-highlight-secondary">
|
||||
{titleCase(segment)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<div className="flex items-center justify-center p-1 text-xl">
|
||||
<Icon name="flag-line" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded-full bg-highlight-primary px-4 py-2">
|
||||
T
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
22
ui/dashboard/Wrapper.tsx
Normal file
22
ui/dashboard/Wrapper.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { useState, type ReactNode } from 'react';
|
||||
|
||||
import Sidenav from './Sidenav';
|
||||
import Usernav from './Usernav';
|
||||
|
||||
export default function Wrapper({ children }: { children: ReactNode }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
return (
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidenav isOpen={isOpen} toggle={toggle} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<Usernav isOpen={isOpen} toggle={toggle} />
|
||||
<div className="flex w-full flex-1 transform flex-col overflow-y-scroll p-8 duration-300 ease-in-out">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Add table
Reference in a new issue