Added basic data fetch & ui update
This commit is contained in:
parent
18d1d82659
commit
cb899d416c
22 changed files with 579 additions and 103 deletions
|
@ -1,16 +1,11 @@
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
import Sidenav from '@/ui/dashboard/Sidenav';
|
import Wrapper from '@/ui/dashboard/Wrapper';
|
||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<div className="flex flex-1 flex-col overflow-hidden sm:flex-row">
|
<Wrapper>{children}</Wrapper>
|
||||||
<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>
|
|
||||||
</div>
|
</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 '@/styles/globals.css';
|
||||||
import 'remixicon/fonts/remixicon.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 { cn } from '@/lib/utils';
|
||||||
import { type ReactNode } from 'react';
|
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 }) {
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
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 />
|
<head />
|
||||||
<body className="relative min-h-screen">
|
<body className="relative min-h-screen">
|
||||||
<main>{children}</main>
|
<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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
experimental: {
|
experimental: {
|
||||||
appDir: true
|
appDir: true
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.43.1",
|
||||||
"remixicon": "^2.5.0",
|
"remixicon": "^2.5.0",
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.0.3",
|
||||||
"tailwind-merge": "^1.9.0",
|
"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
|
prettier-plugin-tailwindcss: ^0.2.2
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0
|
react-dom: 18.2.0
|
||||||
|
react-hook-form: ^7.43.1
|
||||||
remixicon: ^2.5.0
|
remixicon: ^2.5.0
|
||||||
swr: ^2.0.3
|
swr: ^2.0.3
|
||||||
tailwind-merge: ^1.9.0
|
tailwind-merge: ^1.9.0
|
||||||
|
@ -33,6 +34,7 @@ dependencies:
|
||||||
next: 13.1.6_biqbaboplfbrettd7655fr4n2y
|
next: 13.1.6_biqbaboplfbrettd7655fr4n2y
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0_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
|
remixicon: 2.5.0
|
||||||
swr: 2.0.3_react@18.2.0
|
swr: 2.0.3_react@18.2.0
|
||||||
tailwind-merge: 1.9.0
|
tailwind-merge: 1.9.0
|
||||||
|
@ -2268,6 +2270,15 @@ packages:
|
||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
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:
|
/react-is/16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
|
@ -7,9 +9,84 @@ module.exports = {
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans],
|
||||||
|
code: ['var(--font-code)', ...defaultTheme.fontFamily.serif]
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
'light-dark': '#282828',
|
...require('tailwindcss/colors'),
|
||||||
dark: '#202020'
|
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<
|
const Button = forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
kind?: 'default' | 'outline' | 'danger';
|
kind?: 'default' | 'danger' | 'brand';
|
||||||
}
|
}
|
||||||
>(({ kind = 'default', className, ...props }, ref) => (
|
>(({ kind = 'default', className, ...props }, ref) => (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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-highlight-primary hover:bg-highlight-primary/60': kind === 'default',
|
||||||
'bg-red-600 hover:bg-red-600/60': kind === 'danger'
|
'bg-error hover:bg-error/60': kind === 'danger',
|
||||||
|
'bg-brand hover:bg-brand/60': kind === 'brand'
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -14,7 +14,7 @@ const Input = forwardRef<
|
||||||
<Label label={label} description={description} required={props.required} className={className}>
|
<Label label={label} description={description} required={props.required} className={className}>
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</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 { 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 { useState } from 'react';
|
|
||||||
import Logo from '../../public/logo.webp';
|
|
||||||
import AppLink from '../AppLink';
|
import AppLink from '../AppLink';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
|
|
||||||
export default function Sidenav() {
|
export default function Sidenav({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
function toggleSidenav() {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
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,
|
'bottom-0 -translate-x-full sm:translate-x-0': !isOpen,
|
||||||
'w-full sm:w-28': !isOpen
|
'bottom-0 w-full sm:w-28': isOpen
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row sm:flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex px-4 pt-0 sm:block sm:items-center sm:pt-4">
|
<div className="flex p-6">
|
||||||
<div className="flex items-center justify-between">
|
<AppLink className="truncate" href="/">
|
||||||
<AppLink href="/" className="hidden sm:block">
|
<h1>Peer-at Code</h1>
|
||||||
<Image src={Logo} alt="Peer-at Code Logo" className=" h-10 w-10" />
|
</AppLink>
|
||||||
</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>
|
</div>
|
||||||
<div className="hidden px-4 pt-4 sm:block">
|
<div className=" px-4 ">
|
||||||
<hr className="border-light-dark" />
|
<hr className="border-highlight-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden px-4 pt-4 sm:block">
|
<div className="px-4 pt-4">
|
||||||
<ul className="space-y-4">
|
<ul className="space-y-4">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<li key={item.slug}>
|
<li key={item.slug}>
|
||||||
<NavItem item={item} isOpen={isOpen} />
|
<NavItem item={item} isOpen={isOpen} onClick={toggle} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="px-4 pt-4">
|
||||||
<div className="flex flex-row sm:flex-col">
|
<hr className="border-highlight-primary" />
|
||||||
<div className="px-4 py-4">
|
</div>
|
||||||
<button className="flex w-full items-center space-x-2 truncate rounded bg-light-dark p-3">
|
<div className="px-4 pt-4">
|
||||||
<Icon className="text-2xl" name="user-line" />
|
<ul className="space-y-4">
|
||||||
<span className="truncate">Hacktiviste</span>
|
<li>
|
||||||
</button>
|
<NavItem
|
||||||
|
item={{
|
||||||
|
name: 'Tutoriels',
|
||||||
|
slug: '/dashboard/tutorials',
|
||||||
|
icon: 'question-line',
|
||||||
|
disabled: false
|
||||||
|
}}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClick={toggle}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavItem({ item, isOpen }: { item: NavItem; isOpen: boolean }) {
|
function NavItem({
|
||||||
|
item,
|
||||||
|
isOpen,
|
||||||
|
onClick
|
||||||
|
}: {
|
||||||
|
item: NavItem;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) {
|
||||||
const segment = useSelectedLayoutSegment();
|
const segment = useSelectedLayoutSegment();
|
||||||
const isActive = segment?.split('/').pop() === item.slug || (item.slug === '' && !segment);
|
const isActive = segment?.split('/').pop() === item.slug || (item.slug === '' && !segment);
|
||||||
return (
|
return (
|
||||||
<AppLink
|
<AppLink
|
||||||
href={item.disabled ? '/dashboard' : `/dashboard/${item.slug}`}
|
href={item.disabled ? '/dashboard' : `/dashboard/${item.slug}`}
|
||||||
className={cn('flex rounded-md px-3 py-3 text-sm font-medium', {
|
className={cn('flex justify-center rounded-md px-3 py-3 text-sm md:justify-start', {
|
||||||
'bg-light-dark text-gray-400 hover:bg-light-dark/60 hover:text-white': !isActive,
|
'text-muted hover:text-white': !isActive,
|
||||||
'bg-blue-500 text-white': isActive,
|
'bg-highlight-primary text-secondary': isActive,
|
||||||
'text-gray-600 hover:text-gray-600': item.disabled,
|
'text-gray-600 hover:text-gray-600': item.disabled,
|
||||||
'justify-start': isOpen,
|
'justify-center md:justify-start': isOpen,
|
||||||
'justify-center': !isOpen
|
'justify-start sm:justify-center': !isOpen
|
||||||
})}
|
})}
|
||||||
|
onClick={onClick}
|
||||||
passHref
|
passHref
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center space-x-2">
|
||||||
className={cn('flex items-center', {
|
|
||||||
'space-x-2': isOpen
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon className="text-2xl" name={item.icon} />
|
<Icon className="text-2xl" name={item.icon} />
|
||||||
<span
|
<span
|
||||||
className={cn({
|
className={cn('hidden md:block', {
|
||||||
|
'block sm:hidden': isOpen,
|
||||||
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