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

View File

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

View File

@@ -1,3 +1,5 @@
"use client";
import { useCallback } from "react";
import Link from "next/link";
@@ -6,38 +8,34 @@ import { cn } from "@/lib/style";
import * as icons from "@/components/icons";
import { Menu } from "@/components/icons";
import { ThemeToggle } from "@/components/theme-toggle";
import { SidebarStatus, useSetSidebarStatus, useSidebarStatus } from "@/contexts/sidebar";
type Props = {
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(() => {
onMenuToggle?.(!isMenuOpen);
}, [isMenuOpen, onMenuToggle]);
setSidebarStatus(
sidebarStatus === SidebarStatus.Open ? SidebarStatus.Closed : SidebarStatus.Open
);
}, [sidebarStatus, setSidebarStatus]);
return (
<header className={cn("flex items-center justify-between px-4", className)}>
<div className="flex items-baseline gap-x-2.5">
<button type="button" className="flex items-center self-center" onClick={handleMenuToggle}>
<div className="flex items-center gap-x-2.5">
<button
type="button"
className="flex items-center rounded p-1.5 hover:bg-accent"
onClick={handleMenuToggle}
>
<Menu />
</button>
<Link className="text-lg" href="/">
{siteConfig.name}
</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 className="flex gap-x-1">
<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} */
const nextConfig = {
output: "export",
reactStrictMode: true,
experimental: {
typedRoutes: true,

View File

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

19
pnpm-lock.yaml generated
View File

@@ -67,6 +67,9 @@ dependencies:
next:
specifier: 13.5.4
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:
specifier: ^0.2.1
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==}
dev: false
/js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3986,6 +3994,17 @@ packages:
resolution: {integrity: sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA==}
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):
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies: