diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index cc82104..d132f03 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -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 (
-
- -
- {children} -
-
+ {children}
); } diff --git a/app/dashboard/puzzles/[id]/page.tsx b/app/dashboard/puzzles/[id]/page.tsx new file mode 100644 index 0000000..659f9ca --- /dev/null +++ b/app/dashboard/puzzles/[id]/page.tsx @@ -0,0 +1,16 @@ +import Puzzle from '@/ui/Puzzle'; + +export default async function Page({ params }: { params: { id: string } }) { + const { id } = params; + + return ( + // + + // + ); +} + +// export async function generateStaticParams() { +// const { puzzles } = await getPuzzles(); +// return puzzles.map(({ id }) => ({ params: { id } })); +// } diff --git a/app/dashboard/puzzles/page.tsx b/app/dashboard/puzzles/page.tsx index e5e4acf..ac633e1 100644 --- a/app/dashboard/puzzles/page.tsx +++ b/app/dashboard/puzzles/page.tsx @@ -1,39 +1,18 @@ -import Button from '@/ui/Button'; -import Input from '@/ui/Input'; -import ToHTML from '@/ui/ToHTML'; +import Puzzles from '@/ui/Puzzles'; +import SWRFallback from '@/ui/SWRFallback'; + +export default async function Page() { + // const puzzles = await getPuzzles(); -export default function Page() { - // on va utiliser react-hook-form https://react-hook-form.com/ return (
-
-

Titre du puzzle

-

Chapitre 2 - Les boucles à boucler

+
+
+ {/* */} + + {/* */} +
-
- -
-
-
- - -
- -
); } diff --git a/app/fonts/Karrik-Regular.woff2 b/app/fonts/Karrik-Regular.woff2 new file mode 100644 index 0000000..8754815 Binary files /dev/null and b/app/fonts/Karrik-Regular.woff2 differ diff --git a/app/fonts/Typefesse_Claire-Obscure.woff2 b/app/fonts/Typefesse_Claire-Obscure.woff2 new file mode 100644 index 0000000..42a7dce Binary files /dev/null and b/app/fonts/Typefesse_Claire-Obscure.woff2 differ diff --git a/app/fonts/Typefesse_Pleine.woff2 b/app/fonts/Typefesse_Pleine.woff2 new file mode 100644 index 0000000..4b47d54 Binary files /dev/null and b/app/fonts/Typefesse_Pleine.woff2 differ diff --git a/app/layout.tsx b/app/layout.tsx index 379035b..6e68694 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - +
{children}
diff --git a/lib/hooks/use-puzzles.ts b/lib/hooks/use-puzzles.ts new file mode 100644 index 0000000..5179084 --- /dev/null +++ b/lib/hooks/use-puzzles.ts @@ -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)); +} diff --git a/lib/puzzles.ts b/lib/puzzles.ts new file mode 100644 index 0000000..b0cd355 --- /dev/null +++ b/lib/puzzles.ts @@ -0,0 +1,74 @@ +export const getChapters = async (): Promise => { + 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 => { + 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 => { + 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; +}; diff --git a/next.config.js b/next.config.js index b859826..d83edcb 100644 --- a/next.config.js +++ b/next.config.js @@ -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 diff --git a/package.json b/package.json index 1fb1da7..9858462 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 317a6a7..11eab58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/tailwind.config.js b/tailwind.config.js index 6713391..a342765 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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%)' } } }, diff --git a/ui/Button.tsx b/ui/Button.tsx index 86f62e3..2ec101f 100644 --- a/ui/Button.tsx +++ b/ui/Button.tsx @@ -4,16 +4,17 @@ import { forwardRef } from 'react'; const Button = forwardRef< HTMLButtonElement, React.ButtonHTMLAttributes & { - kind?: 'default' | 'outline' | 'danger'; + kind?: 'default' | 'danger' | 'brand'; } >(({ kind = 'default', className, ...props }, ref) => ( + +
+ ); +} diff --git a/ui/Puzzles.tsx b/ui/Puzzles.tsx new file mode 100644 index 0000000..560a255 --- /dev/null +++ b/ui/Puzzles.tsx @@ -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) => ( +
+
+

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

+
+
+
+
+ +
+ ))} + + ); +} diff --git a/ui/SWRFallback.tsx b/ui/SWRFallback.tsx new file mode 100644 index 0000000..ba5d09d --- /dev/null +++ b/ui/SWRFallback.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { SWRConfig } from 'swr'; + +export default function SWRFallback({ + children, + fallback +}: { + children: React.ReactNode; + fallback: { + [key: string]: unknown; + }; +}) { + return {children}; +} diff --git a/ui/UserAuthForm.tsx b/ui/UserAuthForm.tsx new file mode 100644 index 0000000..03c304b --- /dev/null +++ b/ui/UserAuthForm.tsx @@ -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 ( + <> + + + ); +} + +type FormData = { + email?: string; + username: string; + password: string; +}; + +function AuthForm() { + const { + register, + handleSubmit, + formState: { errors }, + setError + } = useForm({ + 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 ( +
+ {!isSignIn && ( + + // {translations.noAccountAssociated}{' '} + // + // {translations.signUpQuestion} + // + // + // ) : ( + // errors.email.message + // )) + } + {...register('email')} + /> + )} + + + + {/* {!isSignIn && ( +

+ En cliquant sur continuer, vous acceptez les{' '} + + Politique de confidentialité + + . +

+ )} */} +

+ {isSignIn ? "Vous n'avez pas de compte?" : 'Vous possédez un compte?'}{' '} + + {isSignIn ? "S'inscrire maintenant" : 'Se connecter'} + +

+
+ ); +} diff --git a/ui/dashboard/Sidenav.tsx b/ui/dashboard/Sidenav.tsx index a72bbb7..dbaf5ec 100644 --- a/ui/dashboard/Sidenav.tsx +++ b/ui/dashboard/Sidenav.tsx @@ -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 ( ); } -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 ( -
+
diff --git a/ui/dashboard/Usernav.tsx b/ui/dashboard/Usernav.tsx new file mode 100644 index 0000000..6a245ea --- /dev/null +++ b/ui/dashboard/Usernav.tsx @@ -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 ( +
+
+
+ +
+ {segment && ( +
+ {titleCase(segment)} +
+ )} +
+
+
+ +
+
+ T +
+
+
+ ); +} diff --git a/ui/dashboard/Wrapper.tsx b/ui/dashboard/Wrapper.tsx new file mode 100644 index 0000000..dd65d66 --- /dev/null +++ b/ui/dashboard/Wrapper.tsx @@ -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 ( +
+ +
+ +
+ {children} +
+
+
+ ); +}