1- import { createFileRoute } from "@tanstack/react-router" ;
1+ import {
2+ createFileRoute ,
3+ Link ,
4+ Outlet ,
5+ useNavigate ,
6+ useRouterState ,
7+ } from "@tanstack/react-router" ;
8+ import { useEffect , useMemo , useState } from "react" ;
9+ import { AppShellSkeleton } from "@/web/components/skeletons/app-skeleton" ;
10+ import {
11+ Sheet ,
12+ SheetContent ,
13+ SheetDescription ,
14+ SheetHeader ,
15+ SheetTitle ,
16+ } from "@/web/components/ui/sheet" ;
17+ import { useIsMobile } from "@/web/hooks/use-mobile" ;
18+ import { useUserSettings } from "@/web/hooks/use-user-settings" ;
19+ import { SETTINGS_NAV_ITEMS } from "@/web/lib/constants" ;
220import { requireAuthenticated } from "@/web/lib/route-guards" ;
21+ import { cn } from "@/web/utils/cn" ;
322import { SettingsError } from "./-components/settings-error" ;
423
524export const Route = createFileRoute ( "/settings" ) ( {
@@ -10,4 +29,132 @@ export const Route = createFileRoute("/settings")({
1029 } ) ;
1130 } ,
1231 errorComponent : SettingsError ,
32+ component : SettingsLayout ,
33+ pendingComponent : AppShellSkeleton ,
1334} ) ;
35+
36+ function SettingsLayout ( ) {
37+ const navigate = useNavigate ( ) ;
38+ const location = useRouterState ( { select : ( state ) => state . location } ) ;
39+ const isMobile = useIsMobile ( ) ;
40+ const [ mobileSidebarOpen , setMobileSidebarOpen ] = useState ( false ) ;
41+
42+ const activePath = location . pathname ?? "" ;
43+
44+ // Use shared settings hook
45+ const settingsQuery = useUserSettings ( ) ;
46+
47+ const navigationItems = useMemo ( ( ) => {
48+ return SETTINGS_NAV_ITEMS . map ( ( item ) => {
49+ const isActive =
50+ activePath === item . to || activePath . startsWith ( `${ item . to } /` ) ;
51+ return { ...item , isActive } ;
52+ } ) ;
53+ } , [ activePath ] ) ;
54+
55+ useEffect ( ( ) => {
56+ const pathname = location . pathname ?? "" ;
57+ if ( pathname === "/settings" || pathname === "/settings/" ) {
58+ void navigate ( { to : "/settings/profile" , replace : true } ) ;
59+ }
60+ } , [ location . pathname , navigate ] ) ;
61+
62+ useEffect ( ( ) => {
63+ const handler = ( ) => setMobileSidebarOpen ( true ) ;
64+ window . addEventListener (
65+ "better-chat:open-settings" ,
66+ handler as EventListener ,
67+ ) ;
68+ return ( ) =>
69+ window . removeEventListener (
70+ "better-chat:open-settings" ,
71+ handler as EventListener ,
72+ ) ;
73+ } , [ ] ) ;
74+
75+ useEffect ( ( ) => {
76+ if ( ! isMobile ) {
77+ setMobileSidebarOpen ( false ) ;
78+ }
79+ } , [ isMobile ] ) ;
80+
81+ const renderNavigation = ( onNavigate ?: ( ) => void ) => (
82+ < div className = "space-y-2" >
83+ { navigationItems . map ( ( item ) => (
84+ < Link
85+ key = { item . to }
86+ to = { item . to }
87+ className = { cn (
88+ "block border px-3 py-3 transition" ,
89+ item . isActive
90+ ? "border-primary bg-primary/10 text-primary"
91+ : "border-transparent bg-muted/40 text-foreground hover:border-muted hover:bg-muted" ,
92+ ) }
93+ preload = "intent"
94+ onClick = { ( ) => onNavigate ?.( ) }
95+ >
96+ < div className = "font-semibold text-sm" > { item . label } </ div >
97+ < p className = "text-muted-foreground text-xs" > { item . description } </ p >
98+ </ Link >
99+ ) ) }
100+ </ div >
101+ ) ;
102+
103+ return (
104+ < div className = "max-w-[100vw] overflow-x-hidden px-2 pt-20 sm:px-4" >
105+ { isMobile && (
106+ < Sheet open = { mobileSidebarOpen } onOpenChange = { setMobileSidebarOpen } >
107+ < SheetContent
108+ side = "left"
109+ className = "w-[min(90vw,18rem)] border-r-0 p-0 sm:max-w-xs"
110+ >
111+ < SheetHeader className = "sr-only" >
112+ < SheetTitle > Settings</ SheetTitle >
113+ < SheetDescription >
114+ Navigate between settings sections.
115+ </ SheetDescription >
116+ </ SheetHeader >
117+ < div className = "flex h-full flex-col gap-4 overflow-hidden bg-card p-4" >
118+ < div className = "space-y-3" >
119+ < h2 className = "font-semibold text-muted-foreground text-sm uppercase tracking-wide" >
120+ Settings
121+ </ h2 >
122+ </ div >
123+ < div className = "-mx-1 flex-1 overflow-y-auto px-1" >
124+ { renderNavigation ( ( ) => setMobileSidebarOpen ( false ) ) }
125+ </ div >
126+ </ div >
127+ </ SheetContent >
128+ </ Sheet >
129+ ) }
130+ < div
131+ className = { cn (
132+ "mx-auto flex min-h-[calc(100svh-5rem-1.5rem)] w-full min-w-0 gap-2 px-1 sm:gap-4 sm:px-0 md:min-h-[calc(100svh-5rem-0.5rem)]" ,
133+ settingsQuery . data ?. chatWidth === "comfortable"
134+ ? "max-w-7xl"
135+ : "max-w-5xl" ,
136+ ) }
137+ >
138+ < aside className = "relative hidden w-64 shrink-0 md:block" >
139+ < div className = "sticky top-20 flex h-[calc(100svh-5rem-1.5rem)] flex-col overflow-hidden border bg-card p-3 shadow-sm sm:p-4 md:h-[calc(100svh-5rem-0.5rem)]" >
140+ < div className = "mb-4" >
141+ < h2 className = "font-semibold text-muted-foreground text-sm uppercase tracking-wide" >
142+ Settings
143+ </ h2 >
144+ </ div >
145+ < div className = "-mx-1 flex-1 overflow-y-auto px-1" >
146+ { renderNavigation ( ) }
147+ </ div >
148+ </ div >
149+ </ aside >
150+ < section className = "min-w-0 flex-1 basis-0" >
151+ < div className = "sticky top-20 flex h-[calc(100svh-5rem-1.5rem)] max-w-[100vw] flex-col overflow-hidden border bg-card shadow-sm md:h-[calc(100svh-5rem-0.5rem)]" >
152+ < div className = "flex-1 overflow-y-auto px-4 py-6 sm:px-6" >
153+ < Outlet />
154+ </ div >
155+ </ div >
156+ </ section >
157+ </ div >
158+ </ div >
159+ ) ;
160+ }
0 commit comments