feat(settings): add settings modal with sidebar trigger

This commit is contained in:
Nicholai Vogel 2026-01-24 13:04:24 -07:00
parent 6ac1abfa86
commit a730220aef
4 changed files with 208 additions and 1 deletions

View File

@ -1,6 +1,7 @@
import { AppSidebar } from "@/components/app-sidebar"
import { SiteHeader } from "@/components/site-header"
import { CommandMenuProvider } from "@/components/command-menu-provider"
import { SettingsProvider } from "@/components/settings-provider"
import {
SidebarInset,
SidebarProvider,
@ -15,6 +16,7 @@ export default async function DashboardLayout({
const projectList = await getProjects()
return (
<SettingsProvider>
<CommandMenuProvider>
<SidebarProvider
defaultOpen={false}
@ -36,5 +38,6 @@ export default async function DashboardLayout({
</SidebarInset>
</SidebarProvider>
</CommandMenuProvider>
</SettingsProvider>
)
}

View File

@ -19,6 +19,7 @@ import { NavFiles } from "@/components/nav-files"
import { NavProjects } from "@/components/nav-projects"
import { NavUser } from "@/components/nav-user"
import { useCommandMenu } from "@/components/command-menu-provider"
import { useSettings } from "@/components/settings-provider"
import {
Sidebar,
SidebarContent,
@ -80,6 +81,7 @@ function SidebarNav({
const pathname = usePathname()
const { state } = useSidebar()
const { open: openSearch } = useCommandMenu()
const { open: openSettings } = useSettings()
const isExpanded = state === "expanded"
const isFilesMode = pathname?.startsWith("/dashboard/files")
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
@ -94,7 +96,11 @@ function SidebarNav({
: "main"
const secondaryItems = [
...data.navSecondary,
...data.navSecondary.map((item) =>
item.title === "Settings"
? { ...item, onClick: openSettings }
: item
),
{ title: "Search", icon: IconSearch, onClick: openSearch },
]

166
src/components/settings-modal.tsx Executable file
View File

@ -0,0 +1,166 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { Separator } from "@/components/ui/separator"
export function SettingsModal({
open,
onOpenChange,
}: {
open: boolean
onOpenChange: (open: boolean) => void
}) {
const { theme, setTheme } = useTheme()
const [emailNotifs, setEmailNotifs] = React.useState(true)
const [pushNotifs, setPushNotifs] = React.useState(true)
const [weeklyDigest, setWeeklyDigest] = React.useState(false)
const [timezone, setTimezone] = React.useState("America/New_York")
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Manage your app preferences.
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="general" className="mt-2">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="notifications">
Notifications
</TabsTrigger>
<TabsTrigger value="appearance">Appearance</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4 pt-4">
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<Select value={timezone} onValueChange={setTimezone}>
<SelectTrigger id="timezone" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="America/New_York">
Eastern (ET)
</SelectItem>
<SelectItem value="America/Chicago">
Central (CT)
</SelectItem>
<SelectItem value="America/Denver">
Mountain (MT)
</SelectItem>
<SelectItem value="America/Los_Angeles">
Pacific (PT)
</SelectItem>
<SelectItem value="Europe/London">
London (GMT)
</SelectItem>
<SelectItem value="Europe/Berlin">
Berlin (CET)
</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Weekly digest</Label>
<p className="text-muted-foreground text-sm">
Receive a summary of activity each week.
</p>
</div>
<Switch
checked={weeklyDigest}
onCheckedChange={setWeeklyDigest}
/>
</div>
</TabsContent>
<TabsContent
value="notifications"
className="space-y-4 pt-4"
>
<div className="flex items-center justify-between">
<div>
<Label>Email notifications</Label>
<p className="text-muted-foreground text-sm">
Get notified about project updates via email.
</p>
</div>
<Switch
checked={emailNotifs}
onCheckedChange={setEmailNotifs}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Push notifications</Label>
<p className="text-muted-foreground text-sm">
Receive push notifications in your browser.
</p>
</div>
<Switch
checked={pushNotifs}
onCheckedChange={setPushNotifs}
/>
</div>
</TabsContent>
<TabsContent
value="appearance"
className="space-y-4 pt-4"
>
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={theme ?? "light"}
onValueChange={setTheme}
>
<SelectTrigger id="theme" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
)
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import { SettingsModal } from "@/components/settings-modal"
const SettingsContext = React.createContext<{
open: () => void
}>({ open: () => {} })
export function useSettings() {
return React.useContext(SettingsContext)
}
export function SettingsProvider({
children,
}: {
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = React.useState(false)
const value = React.useMemo(
() => ({ open: () => setIsOpen(true) }),
[]
)
return (
<SettingsContext.Provider value={value}>
{children}
<SettingsModal open={isOpen} onOpenChange={setIsOpen} />
</SettingsContext.Provider>
)
}