mirror of
https://github.com/ershisan99/DevToysWeb.git
synced 2025-12-16 12:32:48 +00:00
feat: add a toggle for sidebar
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
24
components/client-layout.tsx
Normal file
24
components/client-layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
35
contexts/sidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "export",
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
typedRoutes: true,
|
||||
|
||||
@@ -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
19
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user