feat: add a toggle for sidebar

This commit is contained in:
2024-05-17 13:55:37 +02:00
parent c98817e59d
commit c694b7dafd
9 changed files with 119 additions and 32 deletions

View File

@@ -1,15 +1,20 @@
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Metadata } from "next"; import { Metadata } from "next";
import { CookiesProvider } from "next-client-cookies/server";
import { siteConfig } from "@/config/site"; import { siteConfig } from "@/config/site";
import { fontMono, fontSans } from "@/lib/fonts"; import { fontMono, fontSans } from "@/lib/fonts";
import { cn } from "@/lib/style"; import { cn } from "@/lib/style";
import { ClientLayout } from "@/components/client-layout";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { SiteHeader } from "@/components/site-header"; import { SiteHeader } from "@/components/site-header";
import { TailwindIndicator } from "@/components/tailwind-indicator"; import { TailwindIndicator } from "@/components/tailwind-indicator";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { SearchTextProvider } from "@/contexts/search-text"; import { SearchTextProvider } from "@/contexts/search-text";
import { SidebarProvider } from "@/contexts/sidebar";
export const dynamic = "force-dynamic";
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(siteConfig.url), metadataBase: new URL(siteConfig.url),
@@ -54,16 +59,22 @@ export default function RootLayout({ children }: RootLayoutProps) {
fontMono.variable fontMono.variable
)} )}
> >
<ThemeProvider attribute="class" defaultTheme="system" disableTransitionOnChange> <CookiesProvider>
<SearchTextProvider> <ThemeProvider attribute="class" defaultTheme="system" disableTransitionOnChange>
<div className="grid h-full grid-cols-[18rem_1fr] grid-rows-[3.5rem_1fr]"> <SearchTextProvider>
<SiteHeader className="col-span-full" /> <SidebarProvider>
<Sidebar /> <ClientLayout>
<main className="overflow-y-auto rounded-tl-md border bg-page p-12">{children}</main> <SiteHeader className="col-span-full" />
</div> <Sidebar />
<TailwindIndicator /> <main className="overflow-y-auto rounded-tl-md border bg-page p-12">
</SearchTextProvider> {children}
</ThemeProvider> </main>
</ClientLayout>
<TailwindIndicator />
</SidebarProvider>
</SearchTextProvider>
</ThemeProvider>
</CookiesProvider>
</body> </body>
</html> </html>
); );

View File

@@ -0,0 +1,24 @@
"use client";
import React, { PropsWithChildren } from "react";
import { cn } from "@/lib/style";
import { SidebarStatus, useSidebarStatus } from "@/contexts/sidebar";
export function ClientLayout({ children }: PropsWithChildren) {
const sidebarStatus = useSidebarStatus();
const isOpen = sidebarStatus === SidebarStatus.Open;
const isClosed = sidebarStatus === SidebarStatus.Closed;
return (
<div
className={cn(
"grid h-full grid-rows-[3.5rem_1fr] transition-all",
isOpen && "grid-cols-[18rem_1fr]",
isClosed && "grid-cols-[0_1fr]"
)}
>
{children}
</div>
);
}

View File

@@ -7,11 +7,11 @@ import { ToolGroups } from "./sidebar/tool-groups";
export function Sidebar() { export function Sidebar() {
return ( return (
<nav className="flex flex-col overflow-y-auto"> <nav className="flex flex-col overflow-y-auto overflow-x-hidden">
<div className="mt-px px-4"> <div className="mt-px px-4">
<SearchBar /> <SearchBar />
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto overflow-x-hidden">
<div className="mt-2 p-2"> <div className="mt-2 p-2">
<AllTools /> <AllTools />
</div> </div>

View File

@@ -18,7 +18,7 @@ function RawToolLink({ Icon, shortTitle: title, href, onClick, highlight, groupe
return ( return (
<Link <Link
className={cn( className={cn(
"flex h-10 items-center gap-3 rounded", "flex h-10 items-center gap-3 whitespace-nowrap rounded",
highlight === "both" && "bg-accent", highlight === "both" && "bg-accent",
grouped && "pl-8 -outline-offset-1", // -outline-offset-1: ugly hack for Chrome outlines grouped && "pl-8 -outline-offset-1", // -outline-offset-1: ugly hack for Chrome outlines
"hover:bg-accent" "hover:bg-accent"

View File

@@ -1,3 +1,5 @@
"use client";
import { useCallback } from "react"; import { useCallback } from "react";
import Link from "next/link"; import Link from "next/link";
@@ -6,38 +8,34 @@ import { cn } from "@/lib/style";
import * as icons from "@/components/icons"; import * as icons from "@/components/icons";
import { Menu } from "@/components/icons"; import { Menu } from "@/components/icons";
import { ThemeToggle } from "@/components/theme-toggle"; import { ThemeToggle } from "@/components/theme-toggle";
import { SidebarStatus, useSetSidebarStatus, useSidebarStatus } from "@/contexts/sidebar";
type Props = { type Props = {
className?: string; className?: string;
isMenuOpen?: boolean;
onMenuToggle?: (newValue: boolean) => void;
}; };
export function SiteHeader({ className, isMenuOpen, onMenuToggle }: Props) { export function SiteHeader({ className }: Props) {
const setSidebarStatus = useSetSidebarStatus();
const sidebarStatus = useSidebarStatus();
const handleMenuToggle = useCallback(() => { const handleMenuToggle = useCallback(() => {
onMenuToggle?.(!isMenuOpen); setSidebarStatus(
}, [isMenuOpen, onMenuToggle]); sidebarStatus === SidebarStatus.Open ? SidebarStatus.Closed : SidebarStatus.Open
);
}, [sidebarStatus, setSidebarStatus]);
return ( return (
<header className={cn("flex items-center justify-between px-4", className)}> <header className={cn("flex items-center justify-between px-4", className)}>
<div className="flex items-baseline gap-x-2.5"> <div className="flex items-center gap-x-2.5">
<button type="button" className="flex items-center self-center" onClick={handleMenuToggle}> <button
type="button"
className="flex items-center rounded p-1.5 hover:bg-accent"
onClick={handleMenuToggle}
>
<Menu /> <Menu />
</button> </button>
<Link className="text-lg" href="/"> <Link className="text-lg" href="/">
{siteConfig.name} {siteConfig.name}
</Link> </Link>
<small className="text-xs">
web clone of{" "}
<a
className="text-link hover:underline"
href={siteConfig.links.devtoys}
target="_blank"
rel="noreferrer"
>
DevToys
</a>
</small>
</div> </div>
<div className="flex gap-x-1"> <div className="flex gap-x-1">
<a <a

35
contexts/sidebar.tsx Normal file
View File

@@ -0,0 +1,35 @@
"use client";
import { createContext, PropsWithChildren, useCallback, useContext } from "react";
import { useCookies } from "next-client-cookies";
const SIDEBAR_COOKIE_NAME = "sidebar";
export enum SidebarStatus {
Open = "open",
Closed = "closed",
}
const SidebarContext = createContext(SidebarStatus.Closed);
const SetSidebarContext = createContext<(newStatus: SidebarStatus) => void>(() => {});
export const useSidebarStatus = () => useContext(SidebarContext);
export const useSetSidebarStatus = () => useContext(SetSidebarContext);
export const SidebarProvider = ({ children }: PropsWithChildren) => {
const cookies = useCookies();
const sidebarStatus = cookies.get(SIDEBAR_COOKIE_NAME) as SidebarStatus;
const setSidebarStatus = useCallback(
(newStatus: SidebarStatus) => {
cookies.set(SIDEBAR_COOKIE_NAME, newStatus);
},
[cookies]
);
return (
<SidebarContext.Provider value={sidebarStatus}>
<SetSidebarContext.Provider value={setSidebarStatus}>{children}</SetSidebarContext.Provider>
</SidebarContext.Provider>
);
};

View File

@@ -1,6 +1,5 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: "export",
reactStrictMode: true, reactStrictMode: true,
experimental: { experimental: {
typedRoutes: true, typedRoutes: true,

View File

@@ -47,6 +47,7 @@
"lucide-react": "^0.221.0", "lucide-react": "^0.221.0",
"neverthrow": "^6.0.0", "neverthrow": "^6.0.0",
"next": "13.5.4", "next": "13.5.4",
"next-client-cookies": "^1.1.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"react": "18.2.0", "react": "18.2.0",
"react-day-picker": "^8.7.1", "react-day-picker": "^8.7.1",

19
pnpm-lock.yaml generated
View File

@@ -67,6 +67,9 @@ dependencies:
next: next:
specifier: 13.5.4 specifier: 13.5.4
version: 13.5.4(@babel/core@7.22.1)(react-dom@18.2.0)(react@18.2.0) version: 13.5.4(@babel/core@7.22.1)(react-dom@18.2.0)(react@18.2.0)
next-client-cookies:
specifier: ^1.1.1
version: 1.1.1(next@13.5.4)(react@18.2.0)
next-themes: next-themes:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1(next@13.5.4)(react-dom@18.2.0)(react@18.2.0) version: 0.2.1(next@13.5.4)(react-dom@18.2.0)(react@18.2.0)
@@ -3744,6 +3747,11 @@ packages:
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
dev: false dev: false
/js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
dev: false
/js-tokens@4.0.0: /js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3986,6 +3994,17 @@ packages:
resolution: {integrity: sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA==} resolution: {integrity: sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA==}
dev: false dev: false
/next-client-cookies@1.1.1(next@13.5.4)(react@18.2.0):
resolution: {integrity: sha512-7/wiTI5RPJcP7c1zh5a9XNkvChihtattUG9dHMvfijtvzgEQXotr6E3AHYD1aXyVqGiZA95y/cWlcEI5EJ31tw==}
peerDependencies:
next: '>= 13.0.0'
react: '>= 16.8.0'
dependencies:
js-cookie: 3.0.5
next: 13.5.4(@babel/core@7.22.1)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
/next-themes@0.2.1(next@13.5.4)(react-dom@18.2.0)(react@18.2.0): /next-themes@0.2.1(next@13.5.4)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies: peerDependencies: