From 63d41c43892e6fc42ba09cd047aa4d83c528ce7d Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sat, 9 May 2026 23:09:33 +0800 Subject: [PATCH] feat(web): app shell with responsive nav + theme provider Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/layout.tsx | 22 ++-- apps/web/src/components/app-shell.tsx | 134 +++++++++++++++++++++ apps/web/src/components/nav-config.ts | 16 +++ apps/web/src/components/theme-provider.tsx | 20 +++ 4 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/components/app-shell.tsx create mode 100644 apps/web/src/components/nav-config.ts create mode 100644 apps/web/src/components/theme-provider.tsx diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 3693817..863030b 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,20 +1,15 @@ import type { Metadata, Viewport } from "next"; import { GeistSans } from "geist/font/sans"; +import { ThemeProvider } from "@/components/theme-provider"; +import { AppShell } from "@/components/app-shell"; +import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; -import { Geist } from "next/font/google"; -import { cn } from "@/lib/utils"; - -const geist = Geist({subsets:['latin'],variable:'--font-sans'}); export const metadata: Metadata = { title: "cm WhatsApp Bot", description: "Self-hosted WhatsApp reminder bot", applicationName: "cm WhatsApp Bot", - appleWebApp: { - capable: true, - title: "cm WA Bot", - statusBarStyle: "default", - }, + appleWebApp: { capable: true, title: "cm WA Bot", statusBarStyle: "default" }, }; export const viewport: Viewport = { @@ -26,8 +21,13 @@ export const viewport: Viewport = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {children} + + + + {children} + + + ); } diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx new file mode 100644 index 0000000..0a0cf8b --- /dev/null +++ b/apps/web/src/components/app-shell.tsx @@ -0,0 +1,134 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { NAV_ITEMS } from "@/components/nav-config"; + +// --------------------------------------------------------------------------- +// Bottom nav (mobile only — hidden sm+) +// --------------------------------------------------------------------------- +function BottomNav() { + const pathname = usePathname(); + + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Sidebar (desktop only — hidden below sm) +// --------------------------------------------------------------------------- +function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Top app bar (mobile only) +// --------------------------------------------------------------------------- +function TopAppBar() { + const pathname = usePathname(); + const currentItem = NAV_ITEMS.find(({ href }) => + href === "/" ? pathname === "/" : pathname.startsWith(href), + ); + const title = currentItem?.label ?? "cm WhatsApp Bot"; + + return ( +
+ {title} +
+ ); +} + +// --------------------------------------------------------------------------- +// AppShell — the outer container +// --------------------------------------------------------------------------- +interface AppShellProps { + children: React.ReactNode; +} + +export function AppShell({ children }: AppShellProps) { + return ( + <> + {/* Desktop sidebar */} + + + {/* Mobile top app bar */} + + + {/* Main content + Mobile: push down for top bar (pt-14), push up for bottom nav (pb-16) + Desktop: push right for sidebar (sm:pl-56), no top/bottom chrome offset */} +
+ {children} +
+ + {/* Mobile bottom nav */} + + + ); +} diff --git a/apps/web/src/components/nav-config.ts b/apps/web/src/components/nav-config.ts new file mode 100644 index 0000000..00b97c2 --- /dev/null +++ b/apps/web/src/components/nav-config.ts @@ -0,0 +1,16 @@ +import { Home, Smartphone, Calendar, Settings } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +export interface NavItem { + key: string; + href: string; + label: string; + icon: LucideIcon; +} + +export const NAV_ITEMS: NavItem[] = [ + { key: "dashboard", href: "/", label: "Dashboard", icon: Home }, + { key: "accounts", href: "/accounts", label: "Accounts", icon: Smartphone }, + { key: "reminders", href: "/reminders", label: "Reminders", icon: Calendar }, + { key: "settings", href: "/settings", label: "Settings", icon: Settings }, +]; diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx new file mode 100644 index 0000000..fcb9b9d --- /dev/null +++ b/apps/web/src/components/theme-provider.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ComponentProps } from "react"; + +type ThemeProviderProps = ComponentProps; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return ( + + {children} + + ); +}