Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { Sidebar } from "@/components/layout/Sidebar"
import { Topbar } from "@/components/layout/Topbar"
import { DashboardShell } from "@/components/layout/DashboardShell"
import { RoutePrefetcher } from "@/components/layout/RoutePrefetcher"
import { Providers } from "@/components/providers"
import { getOpenAlertCount } from "@/lib/alerts"
Expand All @@ -19,17 +18,13 @@ export default async function DashboardLayout({
return (
<Providers>
<RoutePrefetcher />
<div className="flex h-screen overflow-hidden">
<Sidebar
companyName={session.user.name ?? ""}
userEmail={session.user.email ?? ""}
openAlerts={openAlerts}
/>
<div className="flex flex-1 flex-col overflow-hidden">
<Topbar />
<main className="flex flex-1 flex-col min-h-0 overflow-hidden">{children}</main>
</div>
</div>
<DashboardShell
companyName={session.user.name ?? ""}
userEmail={session.user.email ?? ""}
openAlerts={openAlerts}
>
{children}
</DashboardShell>
</Providers>
)
}
33 changes: 33 additions & 0 deletions src/components/layout/DashboardShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client"

import type { ReactNode } from "react"
import { Sidebar } from "./Sidebar"
import { Topbar } from "./Topbar"
import { SidebarProvider } from "./SidebarContext"

interface DashboardShellProps {
companyName: string
userEmail: string
openAlerts: number
children: ReactNode
}

function Shell({ companyName, userEmail, openAlerts, children }: DashboardShellProps) {
return (
<div className="flex h-screen overflow-hidden">
<Sidebar companyName={companyName} userEmail={userEmail} openAlerts={openAlerts} />
<div className="flex flex-1 flex-col overflow-hidden">
<Topbar />
<main className="flex flex-1 flex-col min-h-0 overflow-hidden">{children}</main>
</div>
</div>
)
}

export function DashboardShell(props: DashboardShellProps) {
return (
<SidebarProvider>
<Shell {...props} />
</SidebarProvider>
)
}
44 changes: 42 additions & 2 deletions src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { signOut } from "next-auth/react"
import { useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
import { useSidebar } from "./SidebarContext"

// Auto-close the floating sidebar after this idle delay (no pointer over it).
const AUTO_CLOSE_MS = 4000
import {
LayoutDashboard,
Users,
Expand All @@ -12,6 +17,7 @@ import {
Database,
KeyRound,
LogOut,
PanelLeftClose,
} from "lucide-react"

const navItems = [
Expand All @@ -31,11 +37,45 @@ interface SidebarProps {

export function Sidebar({ companyName, userEmail, openAlerts }: SidebarProps) {
const pathname = usePathname()
const { open, toggle, close } = useSidebar()
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)

const clearTimer = () => {
if (timer.current) clearTimeout(timer.current)
timer.current = null
}
const startTimer = () => {
clearTimer()
timer.current = setTimeout(close, AUTO_CLOSE_MS)
}

// Start the idle countdown whenever the sidebar opens; hovering pauses it.
useEffect(() => {
if (open) startTimer()
else clearTimer()
return clearTimer
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])

return (
<aside className="flex h-screen w-60 shrink-0 flex-col border-r border-sidebar-border bg-sidebar">
<div className="flex h-12 items-center border-b border-sidebar-border px-4">
<aside
onMouseEnter={clearTimer}
onMouseLeave={startTimer}
className={cn(
"fixed inset-y-0 left-0 z-40 flex h-screen w-60 flex-col border-r border-sidebar-border bg-sidebar shadow-[4px_0_24px_-6px_oklch(var(--primary)/0.18)] transition-transform duration-200 ease-out",
open ? "translate-x-0" : "-translate-x-full",
)}
>
<div className="flex h-12 items-center justify-between border-b border-sidebar-border px-4">
<span className="text-sm font-semibold text-sidebar-foreground">DataShield</span>
<button
onClick={toggle}
className="-mr-1 rounded-md p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
title="Close sidebar"
aria-label="Close sidebar"
>
<PanelLeftClose className="size-4" />
</button>
</div>

<nav className="flex-1 space-y-0.5 overflow-y-auto px-2 py-3">
Expand Down
28 changes: 28 additions & 0 deletions src/components/layout/SidebarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client"

import { createContext, useContext, useState, type ReactNode } from "react"

type SidebarContextValue = {
open: boolean
toggle: () => void
close: () => void
}

const SidebarContext = createContext<SidebarContextValue | null>(null)

export function SidebarProvider({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(true)
return (
<SidebarContext.Provider
value={{ open, toggle: () => setOpen((o) => !o), close: () => setOpen(false) }}
>
{children}
</SidebarContext.Provider>
)
}

export function useSidebar() {
const ctx = useContext(SidebarContext)
if (!ctx) throw new Error("useSidebar must be used within SidebarProvider")
return ctx
}
15 changes: 14 additions & 1 deletion src/components/layout/Topbar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client"

import { usePathname } from "next/navigation"
import { PanelLeft } from "lucide-react"
import { useSidebar } from "./SidebarContext"

const pageTitles: Record<string, string> = {
"/dashboard": "Dashboard",
Expand All @@ -20,9 +22,20 @@ function getTitle(pathname: string): string {
export function Topbar() {
const pathname = usePathname()
const title = getTitle(pathname)
const { open, toggle } = useSidebar()

return (
<header className="flex h-12 shrink-0 items-center border-b border-border px-6">
<header className="flex h-12 shrink-0 items-center gap-3 border-b border-border px-6">
{!open && (
<button
onClick={toggle}
className="-ml-2 rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
title="Open sidebar"
aria-label="Open sidebar"
>
<PanelLeft className="size-4" />
</button>
)}
<h1 className="text-sm font-medium text-foreground">{title}</h1>
</header>
)
Expand Down