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 "@/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>
|
||||||
);
|
);
|
||||||
|
|||||||
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() {
|
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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "export",
|
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
experimental: {
|
experimental: {
|
||||||
typedRoutes: true,
|
typedRoutes: true,
|
||||||
|
|||||||
@@ -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
19
pnpm-lock.yaml
generated
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user