Improve dashboard sidebar layout

This commit is contained in:
2026-08-18 00:44:48 -04:00
parent 57a985bcd5
commit f0b1a7355e
9 changed files with 420 additions and 330 deletions
-1
View File
@@ -36,7 +36,6 @@
"@dnd-kit/modifiers": "9.0.0",
"@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2",
"@fontsource-variable/playfair-display": "5.2.8",
"@radix-ui/react-alert-dialog": "1.1.16",
"@radix-ui/react-avatar": "1.1.12",
"@radix-ui/react-checkbox": "1.3.4",
@@ -46,7 +46,11 @@ export function ActiveTimerWidget({
if (intervalRef.current) clearInterval(intervalRef.current);
if (running) {
const tick = () =>
setElapsed(Math.floor((Date.now() - new Date(running.startedAt).getTime()) / 1000));
setElapsed(
Math.floor(
(Date.now() - new Date(running.startedAt).getTime()) / 1000,
),
);
tick();
intervalRef.current = setInterval(tick, 1000);
}
@@ -73,7 +77,10 @@ export function ActiveTimerWidget({
window.location.assign(`/dashboard/invoices/${data.invoice!.id}`),
},
});
} else if (data.outcome === "saved_no_invoice" || data.outcome === "saved_no_client") {
} else if (
data.outcome === "saved_no_invoice" ||
data.outcome === "saved_no_client"
) {
toast.warning("Time saved", { description: message });
} else {
toast.success(message);
@@ -96,12 +103,19 @@ export function ActiveTimerWidget({
<Button
variant="destructive"
size="sm"
aria-label={compact ? "Stop timer" : undefined}
onClick={() => clockOut.mutate({})}
disabled={clockOut.isPending}
className={cn(compact && "h-8 px-2", className)}
className={cn(compact && "size-8 p-0", className)}
>
<Square className={cn("h-3.5 w-3.5", !compact && "mr-1.5")} />
{!compact && (clockOut.isPending ? "Stopping…" : "Stop")}
<Square data-icon={compact ? undefined : "inline-start"} />
{compact ? (
<span className="sr-only">Stop timer</span>
) : clockOut.isPending ? (
"Stopping…"
) : (
"Stop"
)}
</Button>
);
@@ -133,18 +147,18 @@ export function ActiveTimerWidget({
<TooltipTrigger asChild>
<Link
href="/dashboard/time-clock"
className="border-primary/30 bg-primary/5 relative flex h-10 w-10 items-center justify-center rounded-md border transition-colors hover:bg-primary/10"
className="border-primary/30 bg-primary/5 hover:bg-primary/10 relative flex size-10 items-center justify-center rounded-xl border transition-colors"
>
<Clock className="text-primary h-5 w-5" />
<span className="absolute top-1 right-1 flex h-2 w-2">
<span className="bg-primary absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
<span className="bg-primary relative inline-flex h-2 w-2 rounded-full" />
<Clock className="text-primary size-5" />
<span className="absolute top-1 right-1 flex size-2">
<span className="bg-primary absolute inline-flex size-full animate-ping rounded-full opacity-75" />
<span className="bg-primary relative inline-flex size-2 rounded-full" />
</span>
</Link>
</TooltipTrigger>
<TooltipContent
side="right"
className="bg-popover text-popover-foreground border-border max-w-56 space-y-2 border p-3 text-sm [&>svg]:bg-popover [&>svg]:fill-popover"
className="bg-popover text-popover-foreground border-border [&>svg]:bg-popover [&>svg]:fill-popover max-w-56 space-y-2 border p-3 text-sm"
>
<p className="text-sm font-medium">
{description}
@@ -169,10 +183,17 @@ export function ActiveTimerWidget({
</Link>
</p>
) : (
<p className="text-muted-foreground text-xs">No invoice selected</p>
<p className="text-muted-foreground text-xs">
No invoice selected
</p>
)}
<div className="flex gap-2 pt-1">
<Button variant="outline" size="sm" asChild className="h-8 flex-1">
<Button
variant="outline"
size="sm"
asChild
className="h-8 flex-1"
>
<Link href="/dashboard/time-clock">Open</Link>
</Button>
{renderStopButton()}
@@ -185,58 +206,41 @@ export function ActiveTimerWidget({
}
return (
<Card className="border-primary/30 bg-primary/5">
<CardContent className="flex flex-col gap-3 p-3">
<div className="flex items-start gap-2">
<span className="relative mt-1 flex h-2.5 w-2.5 flex-shrink-0">
<span className="bg-primary absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
<span className="bg-primary relative inline-flex h-2.5 w-2.5 rounded-full" />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm leading-snug font-medium">
{description}
{running.client && (
<span className="text-muted-foreground font-normal">
{" "}
· {running.client.name}
</span>
)}
</p>
<p className="text-muted-foreground mt-1 text-xs leading-snug">
{invoiceLabel ? (
<>
Billing to{" "}
<Card className="border-primary/30 bg-primary/5 shadow-none">
<CardContent className="flex flex-col gap-2.5 p-3">
<Link
href={`/dashboard/invoices/${running.invoice!.id}`}
className="text-primary hover:underline"
href="/dashboard/time-clock"
className="focus-visible:ring-ring flex items-center justify-between gap-3 rounded-md outline-hidden focus-visible:ring-2"
>
{invoiceLabel}
</Link>
</>
) : (
<>No invoice selected open time clock to assign</>
)}
{" · "}
<Link href="/dashboard/time-clock" className="text-primary hover:underline">
Time clock
</Link>
</p>
</div>
</div>
<div className="flex flex-col items-center gap-2">
<span className="text-primary text-center font-mono text-xl font-bold tabular-nums">
<span className="flex min-w-0 items-center gap-2">
<span className="relative flex size-2.5 shrink-0">
<span className="bg-primary absolute inline-flex size-full animate-ping rounded-full opacity-75" />
<span className="bg-primary relative inline-flex size-2.5 rounded-full" />
</span>
<span className="text-sm font-medium">Timer running</span>
</span>
<span className="text-primary shrink-0 font-mono text-sm font-bold tabular-nums">
{formatElapsedSeconds(elapsed)}
</span>
<div className="flex w-full flex-col gap-1.5">
<Button variant="outline" size="sm" asChild className="h-8 w-full">
</Link>
<p
className="text-muted-foreground truncate text-xs"
title={description}
>
{description}
{running.client ? ` · ${running.client.name}` : ""}
{invoiceLabel ? ` · ${invoiceLabel}` : ""}
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" asChild className="flex-1">
<Link href="/dashboard/time-clock">
<Clock className="mr-1 h-3.5 w-3.5" />
<Clock data-icon="inline-start" />
Open
</Link>
</Button>
{renderStopButton("w-full")}
</div>
{renderStopButton("flex-1")}
</div>
</CardContent>
</Card>
+1 -7
View File
@@ -41,12 +41,6 @@ const geistSans = localFont({
display: "swap",
});
const playfair = localFont({
src: "../../node_modules/@fontsource-variable/playfair-display/files/playfair-display-latin-wght-normal.woff2",
variable: "--font-playfair",
display: "swap",
});
const geistMono = localFont({
src: "../../public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf",
variable: "--font-geist-mono",
@@ -61,7 +55,7 @@ export default function RootLayout({
suppressHydrationWarning
lang="en"
data-color-mode="system"
className={`${geistSans.variable} ${playfair.variable} ${geistMono.variable}`}
className={`${geistSans.variable} ${geistMono.variable}`}
>
<head>
<AppearanceInitScript />
@@ -1,21 +1,7 @@
"use client";
import { BusinessBrandImage } from "~/components/branding/business-brand-image";
import { Logo } from "~/components/branding/logo";
import { hasBusinessBrandAsset } from "~/lib/business-branding";
import { api } from "~/trpc/react";
export function DashboardBrand({ compact = false }: { compact?: boolean }) {
const { data: business } = api.businesses.getDefault.useQuery();
if (business && hasBusinessBrandAsset(business)) {
return (
<BusinessBrandImage
business={business}
kind={compact ? "icon" : "wordmark"}
decorative
className={compact ? "h-8 w-8" : "h-8 w-36"}
/>
);
}
return <Logo size={compact ? "icon" : "sm"} />;
}
@@ -11,7 +11,12 @@ import { cn } from "~/lib/utils";
import { Menu } from "lucide-react";
import { DashboardBrand } from "~/components/branding/dashboard-brand";
import { Button } from "~/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet";
import {
Sheet,
SheetContent,
SheetTitle,
SheetTrigger,
} from "~/components/ui/sheet";
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
import { OnboardingGuard } from "~/components/layout/onboarding-guard";
@@ -40,10 +45,10 @@ function DashboardContent({ children }: { children: React.ReactNode }) {
<Button
variant="outline"
size="icon"
className="bg-background h-10 w-10 shadow-sm"
className="bg-background size-10 shadow-sm"
suppressHydrationWarning
>
<Menu className="h-5 w-5" />
<Menu />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
@@ -51,10 +56,12 @@ function DashboardContent({ children }: { children: React.ReactNode }) {
<DashboardBrand />
<ActiveTimerWidget compact />
</div>
<SheetContent side="left" className="w-72 p-0">
<div className="sr-only">
<h2 id="mobile-nav-title">Navigation Menu</h2>
</div>
<SheetContent
side="left"
className="w-80 max-w-[90vw] gap-0 p-0"
aria-describedby={undefined}
>
<SheetTitle className="sr-only">Navigation menu</SheetTitle>
<Sidebar mobile onClose={() => setIsMobileOpen(false)} />
</SheetContent>
</Sheet>
@@ -64,7 +71,7 @@ function DashboardContent({ children }: { children: React.ReactNode }) {
suppressHydrationWarning
className={cn(
"min-h-screen min-w-0 flex-1 transition-all duration-300 ease-in-out md:ml-0",
!isOnboarding && (isCollapsed ? "md:ml-16" : "md:ml-64"),
!isOnboarding && (isCollapsed ? "md:ml-20" : "md:ml-72"),
)}
>
{isOnboarding ? (
@@ -13,29 +13,52 @@ const SidebarContext = React.createContext<SidebarContextType | undefined>(
undefined,
);
const SIDEBAR_STORAGE_KEY = "sidebar-collapsed";
const SIDEBAR_CHANGE_EVENT = "beenvoice:sidebar-change";
function getSidebarSnapshot() {
return localStorage.getItem(SIDEBAR_STORAGE_KEY) === "true";
}
function getServerSidebarSnapshot() {
return false;
}
function subscribeToSidebar(callback: () => void) {
const handleStorage = (event: StorageEvent) => {
if (event.key === SIDEBAR_STORAGE_KEY) callback();
};
window.addEventListener("storage", handleStorage);
window.addEventListener(SIDEBAR_CHANGE_EVENT, callback);
return () => {
window.removeEventListener("storage", handleStorage);
window.removeEventListener(SIDEBAR_CHANGE_EVENT, callback);
};
}
function saveSidebarState(collapsed: boolean) {
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(collapsed));
window.dispatchEvent(new Event(SIDEBAR_CHANGE_EVENT));
}
export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [isCollapsed, setIsCollapsed] = React.useState(() => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem("sidebar-collapsed");
return saved ? (JSON.parse(saved) as boolean) : false;
});
const isCollapsed = React.useSyncExternalStore(
subscribeToSidebar,
getSidebarSnapshot,
getServerSidebarSnapshot,
);
const toggleCollapse = React.useCallback(() => {
setIsCollapsed((prev) => {
const next = !prev;
localStorage.setItem("sidebar-collapsed", JSON.stringify(next));
return next;
});
}, []);
saveSidebarState(!isCollapsed);
}, [isCollapsed]);
const expand = React.useCallback(() => {
setIsCollapsed(false);
localStorage.setItem("sidebar-collapsed", JSON.stringify(false));
saveSidebarState(false);
}, []);
const collapse = React.useCallback(() => {
setIsCollapsed(true);
localStorage.setItem("sidebar-collapsed", JSON.stringify(true));
saveSidebarState(true);
}, []);
return (
+248 -169
View File
@@ -1,220 +1,298 @@
"use client";
import type { ComponentType } from "react";
import {
ChevronsUpDown,
LogOut,
PanelLeftClose,
PanelLeftOpen,
Plus,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { authClient } from "~/lib/auth-client";
import { Skeleton } from "~/components/ui/skeleton";
import { Button } from "~/components/ui/button";
import { LogOut, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { getNavigationForUser, isNavLinkActive } from "~/lib/navigation";
import { useSidebar } from "./sidebar-provider";
import { cn } from "~/lib/utils";
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
import { DashboardBrand } from "~/components/branding/dashboard-brand";
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { Separator } from "~/components/ui/separator";
import { Skeleton } from "~/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { getGravatarUrl } from "~/lib/gravatar";
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
import { useAuthSession } from "~/hooks/use-auth-session";
import { useDashboardUser } from "~/components/layout/dashboard-user-context";
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
import { authClient } from "~/lib/auth-client";
import { getGravatarUrl } from "~/lib/gravatar";
import { getNavigationForUser, isNavLinkActive } from "~/lib/navigation";
import { cn } from "~/lib/utils";
import { useDashboardUser } from "./dashboard-user-context";
import { useSidebar } from "./sidebar-provider";
interface SidebarProps {
mobile?: boolean;
onClose?: () => void;
}
export function Sidebar({ mobile, onClose }: SidebarProps) {
interface SidebarLinkProps {
name: string;
href: string;
icon: ComponentType<{ className?: string }>;
active: boolean;
collapsed: boolean;
mobile: boolean;
onClose?: () => void;
}
function SidebarLink({
name,
href,
icon: Icon,
active,
collapsed,
mobile,
onClose,
}: SidebarLinkProps) {
const link = (
<Link
href={href}
aria-current={active ? "page" : undefined}
onClick={mobile ? onClose : undefined}
className={cn(
"group focus-visible:ring-ring relative flex h-10 items-center rounded-xl text-sm font-medium outline-hidden transition-colors focus-visible:ring-2",
collapsed ? "w-11 justify-center" : "gap-3 px-3",
active
? "bg-accent text-accent-foreground shadow-sm"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
)}
>
<Icon className="size-4.5 shrink-0" />
{!collapsed ? (
<>
<span className="min-w-0 flex-1 truncate">{name}</span>
{active ? (
<span className="bg-primary size-1.5 shrink-0 rounded-full" />
) : null}
</>
) : null}
</Link>
);
if (!collapsed) return link;
return (
<Tooltip>
<TooltipTrigger asChild>{link}</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{name}
</TooltipContent>
</Tooltip>
);
}
export function Sidebar({ mobile = false, onClose }: SidebarProps) {
const pathname = usePathname();
const { data: session, isPending } = useAuthSession();
const { isAdmin } = useDashboardUser();
const { isCollapsed, toggleCollapse } = useSidebar();
const navSections = getNavigationForUser(isAdmin);
// If mobile, always expanded
const collapsed = mobile ? false : isCollapsed;
const SidebarContent = (
<div className="flex h-full flex-col justify-between">
<div>
{/* Header / Logo */}
<div
const sidebarContent = (
<TooltipProvider delayDuration={150}>
<div className="flex h-full min-h-0 flex-col">
<header
className={cn(
"mb-2 flex h-14 items-center px-4",
collapsed ? "justify-center px-2" : "justify-between",
"border-border/70 flex h-20 shrink-0 items-center border-b",
collapsed ? "justify-center px-2" : "px-4",
)}
>
{!collapsed && (
<div className="flex items-center gap-2">
<DashboardBrand />
</div>
)}
{collapsed && <DashboardBrand compact />}
{!mobile && !collapsed && (
<div className="h-8 w-8" /> // Spacer to keep alignment if needed, or just remove
)}
</div>
{/* Navigation */}
<nav
<Link
href="/dashboard"
onClick={mobile ? onClose : undefined}
aria-label="Beenvoice dashboard"
className={cn(
"mt-4 flex flex-col gap-6 px-2",
"focus-visible:ring-ring flex min-w-0 items-center outline-hidden focus-visible:ring-2",
collapsed ? "justify-center rounded-lg" : "flex-1",
)}
>
<DashboardBrand compact={collapsed} />
</Link>
</header>
<div className="flex min-h-0 flex-1 flex-col">
<nav
id="dashboard-sidebar-navigation"
aria-label="Dashboard navigation"
className="min-h-0 flex-1 overflow-y-auto px-3 py-4"
>
<div className="flex flex-col gap-5">
<div className={cn("flex", collapsed && "justify-center")}>
{collapsed ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
asChild
size="icon"
className="size-11 rounded-xl"
>
<Link href="/dashboard/invoices/new">
<Plus />
<span className="sr-only">New invoice</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
New invoice
</TooltipContent>
</Tooltip>
) : (
<Button asChild className="w-full justify-start">
<Link
href="/dashboard/invoices/new"
onClick={mobile ? onClose : undefined}
>
<Plus data-icon="inline-start" />
New invoice
</Link>
</Button>
)}
</div>
{navSections.map((section, sectionIndex) => (
<section
key={section.title}
aria-label={collapsed ? section.title : undefined}
aria-labelledby={
collapsed ? undefined : `sidebar-section-${sectionIndex}`
}
className="flex flex-col gap-2"
>
{collapsed ? (
sectionIndex > 0 ? (
<Separator className="mx-auto w-8" />
) : null
) : (
<h2
id={`sidebar-section-${sectionIndex}`}
className="text-muted-foreground px-3 text-[0.6875rem] font-semibold tracking-[0.12em] uppercase"
>
{section.title}
</h2>
)}
<div
className={cn(
"flex flex-col gap-1",
collapsed && "items-center",
)}
>
{navSections.map((section) => (
<div key={section.title}>
{!collapsed && (
<div className="text-muted-foreground/60 mb-2 px-2 text-xs font-semibold tracking-wider uppercase">
{section.title}
{section.links.map((link) => (
<SidebarLink
key={link.href}
{...link}
active={isNavLinkActive(pathname, link.href)}
collapsed={collapsed}
mobile={mobile}
onClose={onClose}
/>
))}
</div>
)}
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1">
{section.links.map((link) => {
const Icon = link.icon;
const isActive = isNavLinkActive(pathname, link.href);
</section>
))}
</div>
</nav>
if (collapsed) {
return (
<TooltipProvider key={link.href} delayDuration={0}>
{!mobile ? (
<div
className={cn(
"shrink-0 px-3 pb-3",
collapsed && "flex justify-center",
)}
>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={link.href}
data-active={isActive ? "true" : undefined}
<Button
type="button"
variant="ghost"
size={collapsed ? "icon" : "default"}
aria-label={
collapsed ? "Expand sidebar" : "Collapse sidebar"
}
aria-expanded={!collapsed}
aria-controls="dashboard-sidebar-navigation"
onClick={toggleCollapse}
className={cn(
"flex h-10 w-10 items-center justify-center rounded-md transition-colors",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
"text-muted-foreground h-10 rounded-xl",
collapsed
? "mx-auto size-11"
: "w-full justify-start gap-3 px-3",
)}
>
<Icon className="h-5 w-5" />
</Link>
{collapsed ? <PanelLeftOpen /> : <PanelLeftClose />}
{!collapsed ? <span>Collapse sidebar</span> : null}
</Button>
</TooltipTrigger>
<TooltipContent
side="right"
className="font-medium"
>
{link.name}
<TooltipContent side="right" sideOffset={8}>
{collapsed ? "Expand sidebar" : "Collapse sidebar"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return (
<Link
key={link.href}
href={link.href}
data-active={isActive ? "true" : undefined}
onClick={mobile ? onClose : undefined}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<Icon className="h-4 w-4" />
{link.name}
</Link>
);
})}
</div>
</div>
</div>
))}
</nav>
</div>
{/* Footer / User */}
<div className="mt-auto space-y-2 p-2">
{!mobile && (
<div
className={cn(
"flex",
collapsed ? "justify-center" : "justify-end px-2",
)}
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground h-8 w-8"
onClick={toggleCollapse}
>
{collapsed ? (
<PanelLeftOpen className="h-4 w-4" />
) : (
<PanelLeftClose className="h-4 w-4" />
)}
</Button>
</div>
)}
) : null}
<footer className="border-border/70 flex shrink-0 flex-col gap-2 border-t p-3">
<ActiveTimerWidget collapsed={collapsed} />
<div
className={cn(
"border-border/50 border-t pt-4",
collapsed ? "flex flex-col items-center gap-2" : "px-2",
)}
>
{isPending ? (
<div
className={cn(
"flex items-center gap-3",
collapsed ? "justify-center" : "px-2",
"flex h-12 items-center gap-3 px-2",
collapsed && "justify-center px-0",
)}
>
<Skeleton className="h-9 w-9 rounded-full" />
{!collapsed && (
<div className="flex-1 space-y-1">
<Skeleton className="size-9 rounded-full" />
{!collapsed ? (
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-2 w-24" />
<Skeleton className="h-2.5 w-28" />
</div>
)}
) : null}
</div>
) : session?.user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size={collapsed ? "icon" : "default"}
aria-label={collapsed ? "Open account menu" : undefined}
className={cn(
"w-full justify-start p-0 hover:bg-transparent",
collapsed && "justify-center",
"h-auto min-h-12 rounded-xl",
collapsed
? "mx-auto size-11 p-0"
: "w-full justify-start gap-3 px-2 py-1.5",
)}
>
{/* FIXED: Changed div to span to prevent hydration error */}
<span
className={cn(
"flex items-center gap-3",
collapsed ? "justify-center" : "w-full",
)}
>
<Avatar className="border-border h-9 w-9 border">
<Avatar className="border-border size-9 shrink-0 border">
<AvatarImage
src={getGravatarUrl(session.user.email)}
alt={session.user.name ?? "User"}
alt=""
/>
<AvatarFallback>
{session.user.name?.[0] ?? "U"}
</AvatarFallback>
</Avatar>
{!collapsed && (
{!collapsed ? (
<>
<span className="min-w-0 flex-1 text-left">
<span className="block truncate text-sm font-medium">
{session.user.name}
@@ -223,57 +301,58 @@ export function Sidebar({ mobile, onClose }: SidebarProps) {
{session.user.email}
</span>
</span>
)}
</span>
<ChevronsUpDown className="text-muted-foreground" />
</>
) : null}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
side={mobile ? "top" : "right"}
align="end"
className="bg-background/80 border-border/50 w-56 backdrop-blur-xl"
sideOffset={10}
sideOffset={8}
className="border-border w-60 border"
>
<DropdownMenuLabel>
<div className="flex flex-col space-y-1">
<p className="text-sm leading-none font-medium">
{session.user.name}
</p>
<p className="text-muted-foreground text-xs leading-none">
<DropdownMenuLabel className="flex flex-col gap-1">
<span className="truncate">{session.user.name}</span>
<span className="text-muted-foreground truncate text-xs font-normal">
{session.user.email}
</p>
</div>
</span>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
variant="destructive"
onClick={async () => {
await authClient.signOut();
window.location.href = "/";
}}
className="text-red-600 focus:bg-red-100/50 focus:text-red-600 dark:focus:bg-red-900/20"
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
<LogOut />
Sign out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
) : null}
</footer>
</div>
</div>
</div>
</TooltipProvider>
);
if (mobile) {
return <div className="bg-background h-full">{SidebarContent}</div>;
return <div className="bg-background h-full">{sidebarContent}</div>;
}
return (
<aside
className={cn(
"border-border bg-background fixed top-0 bottom-0 left-0 z-30 hidden flex-col rounded-none border-r shadow-none transition-all duration-300 ease-in-out md:flex",
isCollapsed ? "w-16" : "w-64",
"border-border bg-background fixed inset-y-0 left-0 z-30 hidden border-r transition-[width] duration-200 ease-out md:block",
isCollapsed ? "w-20" : "w-72",
)}
>
{SidebarContent}
{sidebarContent}
</aside>
);
}
+3 -2
View File
@@ -4,7 +4,8 @@
@layer base {
:root {
--app-font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--app-font-heading: var(--font-playfair), ui-serif, Georgia, serif;
--app-font-heading: var(--font-geist-sans), ui-sans-serif, system-ui,
sans-serif;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
@@ -100,7 +101,7 @@
--color-ring: hsl(var(--ring));
--font-sans: var(--app-font-sans), ui-sans-serif, system-ui, sans-serif;
--font-heading: var(--app-font-heading), ui-serif, Georgia, serif;
--font-heading: var(--app-font-heading), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
--radius-sm: calc(var(--radius) - 4px);
-3
View File
@@ -86,7 +86,6 @@
"@dnd-kit/modifiers": "9.0.0",
"@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2",
"@fontsource-variable/playfair-display": "5.2.8",
"@radix-ui/react-alert-dialog": "1.1.16",
"@radix-ui/react-avatar": "1.1.12",
"@radix-ui/react-checkbox": "1.3.4",
@@ -664,8 +663,6 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@fontsource-variable/playfair-display": ["@fontsource-variable/playfair-display@5.2.8", "", {}, "sha512-ZzVIXPOrL85yyOvZYoBzUszIJM+xKkHqni4IYn2CVLaGQQdJR8sBeC8yFNgjxSJ7ludTwta8qpULeOFuk5X75A=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],