Responsive Sidenav

This commit is contained in:
Théo 2023-02-05 18:28:55 +01:00
parent 887d3f6841
commit 16252ee5a2
12 changed files with 216 additions and 48 deletions

View file

@ -0,0 +1,15 @@
import AppLink from '@/ui/AppLink';
export default function Home() {
return (
<div className="flex h-screen w-full">
<div className="m-auto flex flex-col">
<h1 className="text-center text-4xl font-bold">
Amuse toi avec <span className="rounded-md bg-white p-1 text-black ">Next.js</span> et{' '}
<span className="text-blue-500">Tailwindcss</span> !
</h1>
<AppLink href="/dashboard">Dashboard</AppLink>
</div>
</div>
);
}

View file

@ -0,0 +1,15 @@
import AppLink from '@/ui/AppLink';
export default function Home() {
return (
<div className="flex h-screen w-full">
<div className="m-auto flex flex-col">
<h1 className="text-center text-4xl font-bold">
Amuse toi avec <span className="rounded-md bg-white p-1 text-black ">Next.js</span> et{' '}
<span className="text-blue-500">Tailwindcss</span> !
</h1>
<AppLink href="/dashboard">Dashboard</AppLink>
</div>
</div>
);
}

View file

@ -1,11 +1,6 @@
import Sidenav from '@/ui/dashboard/Sidenav'; import Wrapper from '@/ui/dashboard/Wrapper';
import { type ReactNode } from 'react'; import { type ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) { export default function Layout({ children }: { children: ReactNode }) {
return ( return <Wrapper>{children}</Wrapper>;
<div className="mx-4 min-h-screen md:pl-60 xl:mx-0">
<Sidenav />
<div className="mx-auto h-screen max-w-5xl py-8">{children}</div>
</div>
);
} }

View file

@ -27,7 +27,7 @@ export default function Page() {
label="Code" label="Code"
name="code_file" name="code_file"
type="file" type="file"
accept=".py,.js,.ts,.java,.rust" accept=".py,.js,.ts,.java,.rust,.c"
/> />
</div> </div>
<Button className="mt-6" type="submit"> <Button className="mt-6" type="submit">

View file

@ -1,4 +1,5 @@
import '@/styles/globals.css'; import '@/styles/globals.css';
import 'remixicon/fonts/remixicon.css';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { type ReactNode } from 'react'; import { type ReactNode } from 'react';

View file

@ -8,6 +8,7 @@
export type NavItem = { export type NavItem = {
name: string; name: string;
slug: string; slug: string;
icon: string;
disabled?: boolean; disabled?: boolean;
}; };
@ -19,26 +20,31 @@ export const navItems: NavItem[] = [
{ {
name: 'Dashboard', name: 'Dashboard',
slug: '', slug: '',
icon: 'dashboard-line',
disabled: false disabled: false
}, },
{ {
name: 'Classement', name: 'Classement',
slug: 'ranking', slug: 'ranking',
icon: 'line-chart-line',
disabled: false disabled: false
}, },
{ {
name: 'Puzzles', name: 'Puzzles',
slug: 'puzzles', slug: 'puzzles',
icon: 'code-s-slash-line',
disabled: false disabled: false
}, },
{ {
name: 'Badges', name: 'Badges',
slug: 'badges', slug: 'badges',
icon: 'award-fill',
disabled: false disabled: false
}, },
{ {
name: 'Paramètres', name: 'Paramètres',
slug: 'settings', slug: 'settings',
icon: 'equalizer-line',
disabled: false disabled: false
} }
]; ];

View file

@ -1,5 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
swcMinify: true,
experimental: { experimental: {
appDir: true appDir: true
} }

View file

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
@ -22,11 +22,11 @@
"homepage": "https://github.com/Peer-at-Code/peer-at-code#readme", "homepage": "https://github.com/Peer-at-Code/peer-at-code#readme",
"dependencies": { "dependencies": {
"@next/font": "13.1.6", "@next/font": "13.1.6",
"@tabler/icons": "^2.2.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"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",
"remixicon": "^2.5.0",
"swr": "^2.0.3", "swr": "^2.0.3",
"tailwind-merge": "^1.9.0", "tailwind-merge": "^1.9.0",
"zod": "^3.20.2" "zod": "^3.20.2"

12
pnpm-lock.yaml generated
View file

@ -2,7 +2,6 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@next/font': 13.1.6 '@next/font': 13.1.6
'@tabler/icons': ^2.2.0
'@tailwindcss/forms': ^0.5.3 '@tailwindcss/forms': ^0.5.3
'@types/node': 18.11.18 '@types/node': 18.11.18
'@types/react': 18.0.27 '@types/react': 18.0.27
@ -21,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
remixicon: ^2.5.0
swr: ^2.0.3 swr: ^2.0.3
tailwind-merge: ^1.9.0 tailwind-merge: ^1.9.0
tailwindcss: ^3.2.4 tailwindcss: ^3.2.4
@ -29,11 +29,11 @@ specifiers:
dependencies: dependencies:
'@next/font': 13.1.6 '@next/font': 13.1.6
'@tabler/icons': 2.2.0
clsx: 1.2.1 clsx: 1.2.1
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
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
zod: 3.20.2 zod: 3.20.2
@ -276,10 +276,6 @@ packages:
tslib: 2.5.0 tslib: 2.5.0
dev: false dev: false
/@tabler/icons/2.2.0:
resolution: {integrity: sha512-s2mm+7JqmLObKdU89Dtiy+USmUpOlACsoXZZPykjAJZC4pK3wMYxLsclJxViWLeLTb6Bc0oga92V7R+9nrj1ZQ==}
dev: false
/@tailwindcss/forms/0.5.3_tailwindcss@3.2.4: /@tailwindcss/forms/0.5.3_tailwindcss@3.2.4:
resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==} resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
peerDependencies: peerDependencies:
@ -2314,6 +2310,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/remixicon/2.5.0:
resolution: {integrity: sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==}
dev: false
/resolve-from/4.0.0: /resolve-from/4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}

5
ui/Icon.tsx Normal file
View file

@ -0,0 +1,5 @@
import { cn } from '@/lib/utils';
export default function Icon({ name, className }: { name: string; className?: string }) {
return <i className={cn(`ri-${name}`, className)} role="img" />;
}

View file

@ -1,53 +1,139 @@
'use client';
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 { useSelectedLayoutSegment } from 'next/navigation'; import { useSelectedLayoutSegment } from 'next/navigation';
import AppLink from '../AppLink'; import AppLink from '../AppLink';
import Icon from '../Icon';
export default function Sidenav() { export default function Sidenav({
onClick,
width,
isOpen
}: {
onClick: () => void;
width: number;
isOpen: boolean;
}) {
return ( return (
<div className="fixed top-0 left-0 hidden h-full w-60 space-y-4 bg-dark shadow-md md:block"> (width >= 640 && (
<div className="px-6 pt-4"> <aside
<div className="flex items-center"> className={cn(
<div className="mx-auto"> 'hidden fixed top-0 left-0 z-10 h-full space-y-4 bg-dark shadow-md transition-all duration-300 ease-in-out sm:block',
<AppLink href="/"> isOpen ? 'w-60' : 'w-24 '
<h1 className="text-2xl font-bold">Peer-at Code</h1> )}
</AppLink> >
</div> <div className="px-8 pt-4">
</div> <div className="mx-auto items-center">
</div> <button onClick={onClick} className="flex h-10 w-full items-center space-x-2">
<hr className="mx-auto w-1/2 pb-4" /> <Icon
<ul className="relative flex flex-col space-y-4 px-8"> className={cn(
{navItems.map((item) => ( 'text-xl transition-transform duration-300 ease-in-out',
<li className="relative" key={item.slug}> isOpen ? 'rotate-180' : 'rotate-0'
<NavItem item={item} /> )}
</li> name="arrow-right-s-line"
))} />
</ul> <span
<div className="absolute bottom-5 flex w-full items-center text-center"> className={cn(
<button className="mx-auto rounded-md bg-light-dark py-3 px-8 hover:bg-light-dark/60"> 'text-xl font-semibold transition',
<span className="text-sm font-semibold">Session ici plus tard</span> isOpen ? 'duration-400 opacity-100 delay-150' : 'opacity-0'
)}
>
Peer-at-Code
</span>
</button> </button>
</div> </div>
</div> </div>
<hr className="mx-auto w-1/2 pb-4" />
<ul
className={cn(
'relative flex flex-col space-y-4 transition-all duration-300 ease-in-out',
isOpen ? ' px-8' : 'px-4'
)}
>
{navItems.map((item) => (
<li className="relative" key={item.slug}>
<NavItem item={item} isOpen={isOpen} />
</li>
))}
</ul>
<div
className={cn(
'absolute bottom-5 flex w-full items-center text-center',
isOpen ? 'px-8' : 'px-4'
)}
>
<button
className={cn(
'mx-auto w-full truncate rounded-md bg-light-dark py-3 transition-all duration-300 ease-in-out hover:bg-light-dark/60'
)}
>
<span className="text-sm font-semibold">Session ici</span>
</button>
</div>
</aside>
)) || (
<aside
className={cn(
'fixed top-0 z-10 w-full bg-dark shadow-md transition-all duration-300 ease-in-out sm:block md:hidden'
)}
>
<div className="flex items-center justify-end px-4 py-4">
<div className="flex items-center space-x-4">
<button onClick={onClick} className="flex h-10 w-full items-center space-x-2">
<span className={cn('text-xl font-semibold')}>Peer-at-Code</span>
<Icon
className={cn(
'text-xl transition-transform duration-300 ease-in-out',
isOpen ? 'rotate-180' : 'rotate-0'
)}
name="arrow-up-s-line"
/>
</button>
</div>
</div>
<div
className={cn(
'flex h-screen transform flex-col duration-100',
isOpen ? 'block' : 'hidden'
)}
>
<ul
className={cn(
'relative flex flex-col space-y-4 px-8 transition-all duration-300 ease-in-out'
)}
>
{navItems.map((item) => (
<li className="relative" key={item.slug} onClick={onClick}>
<NavItem item={item} isOpen={isOpen} />
</li>
))}
</ul>
</div>
</aside>
)
); );
} }
function NavItem({ item }: { item: NavItem }) { function NavItem({ item, isOpen }: { item: NavItem; isOpen: boolean }) {
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('block rounded-md px-3 py-3 text-sm font-medium', { className={cn(
'flex rounded-md px-3 py-3 text-sm font-medium transition duration-300 ease-in-out',
{
'bg-light-dark text-gray-400 hover:bg-light-dark/60': !isActive, 'bg-light-dark text-gray-400 hover:bg-light-dark/60': !isActive,
'bg-blue-500 text-white': isActive, 'bg-blue-500 text-white': isActive,
'text-gray-600 hover:text-gray-600': item.disabled 'text-gray-600 hover:text-gray-600': item.disabled,
})} 'justify-center': !isOpen
}
)}
passHref passHref
> >
{item.name} <div className={cn('flex items-center', isOpen && 'space-x-4')}>
<Icon className="text-2xl" name={item.icon} />
<span className={cn(isOpen ? 'block' : 'hidden')}>{item.name}</span>
</div>
</AppLink> </AppLink>
); );
} }

43
ui/dashboard/Wrapper.tsx Normal file
View file

@ -0,0 +1,43 @@
'use client';
import { cn } from '@/lib/utils';
import { useCallback, useEffect, useState, type ReactNode } from 'react';
import Sidenav from './Sidenav';
export default function Wrapper({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [width, setWidth] = useState(0);
function toggleSidenav() {
setIsOpen(!isOpen);
}
function useWidth() {
const handleResize = useCallback(() => setWidth(window.innerWidth), []);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
return width;
}
return (
<div
className={cn(
'relative min-h-screen transition-all duration-300 ease-in-out',
isOpen ? 'sm:pl-60' : 'sm:pl-24'
)}
>
<Sidenav onClick={toggleSidenav} width={useWidth()} isOpen={isOpen} />
<div
className={cn(
'mx-auto h-screen w-full transform px-2 pt-24 pb-8 duration-300 ease-in-out sm:px-0 sm:py-8',
isOpen ? 'md:max-w-6xl' : 'md:max-w-7xl'
)}
>
{children}
</div>
</div>
);
}