11'use client' ;
22
3- import {
4- BugIcon ,
5- FunnelIcon ,
6- GlobeIcon ,
7- HouseIcon ,
8- InfoIcon ,
9- ListIcon ,
10- MapPinIcon ,
11- TargetIcon ,
12- UserIcon ,
13- UsersIcon ,
14- XIcon ,
15- } from '@phosphor-icons/react' ;
163import { useAtom } from 'jotai' ;
17- import Link from 'next/link' ;
18- import { usePathname } from 'next/navigation' ;
19- import { useCallback , useEffect , useState } from 'react' ;
204import { toast } from 'sonner' ;
215import { AnalyticsToolbar } from '@/app/(main)/websites/[id]/_components/analytics-toolbar' ;
22- import { Logo } from '@/components/layout/logo' ;
23- import { SignOutButton } from '@/components/layout/sign-out-button' ;
24- import { ThemeToggle } from '@/components/layout/theme-toggle' ;
25- import { NotificationsPopover } from '@/components/notifications/notifications-popover' ;
26- import { Button } from '@/components/ui/button' ;
27- import { ScrollArea } from '@/components/ui/scroll-area' ;
6+ import { Sidebar } from '@/components/layout/sidebar' ;
287import { cn } from '@/lib/utils' ;
298import { isAnalyticsRefreshingAtom } from '@/stores/jotai/filterAtoms' ;
309
31- const DEMO_WEBSITE_ID = 'OXmNQsViBT-FOS_wZCTHc' ;
32- const DEMO_WEBSITE_URL = 'https://www.databuddy.cc' ;
33-
34- const demoNavigation = [
35- {
36- title : 'Web Analytics' ,
37- items : [
38- {
39- name : 'Overview' ,
40- icon : HouseIcon ,
41- href : `/demo/${ DEMO_WEBSITE_ID } ` ,
42- highlight : true ,
43- } ,
44- {
45- name : 'Sessions' ,
46- icon : UserIcon ,
47- href : `/demo/${ DEMO_WEBSITE_ID } /sessions` ,
48- highlight : true ,
49- } ,
50- {
51- name : 'Errors' ,
52- icon : BugIcon ,
53- href : `/demo/${ DEMO_WEBSITE_ID } /errors` ,
54- highlight : true ,
55- } ,
56- {
57- name : 'Map' ,
58- icon : MapPinIcon ,
59- href : `/demo/${ DEMO_WEBSITE_ID } /map` ,
60- highlight : true ,
61- } ,
62- ] ,
63- } ,
64- {
65- title : 'Product Analytics' ,
66- items : [
67- {
68- name : 'Profiles' ,
69- icon : UsersIcon ,
70- href : `/demo/${ DEMO_WEBSITE_ID } /profiles` ,
71- highlight : true ,
72- } ,
73- {
74- name : 'Funnels' ,
75- icon : FunnelIcon ,
76- href : `/demo/${ DEMO_WEBSITE_ID } /funnels` ,
77- highlight : true ,
78- } ,
79- {
80- name : 'Goals' ,
81- icon : TargetIcon ,
82- href : `/demo/${ DEMO_WEBSITE_ID } /goals` ,
83- highlight : true ,
84- } ,
85- ] ,
86- } ,
87- ] ;
88-
89- function Sidebar ( ) {
90- const pathname = usePathname ( ) ;
91- const [ isMobileOpen , setIsMobileOpen ] = useState ( false ) ;
92-
93- const closeSidebar = useCallback ( ( ) => {
94- setIsMobileOpen ( false ) ;
95- } , [ ] ) ;
96-
97- const handleKeyDown = useCallback (
98- ( e : KeyboardEvent ) => {
99- if ( e . key === 'Escape' && isMobileOpen ) {
100- closeSidebar ( ) ;
101- }
102- } ,
103- [ isMobileOpen , closeSidebar ]
104- ) ;
105-
106- useEffect ( ( ) => {
107- document . addEventListener ( 'keydown' , handleKeyDown ) ;
108- return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
109- } , [ handleKeyDown ] ) ;
110-
111- return (
112- < >
113- < header className = "fixed top-0 right-0 left-0 z-50 h-16 w-full border-b bg-background/95 backdrop-blur-md" >
114- < div className = "flex h-full items-center px-4 md:px-6" >
115- < div className = "flex items-center gap-4" >
116- < Button
117- aria-label = "Toggle menu"
118- className = "md:hidden"
119- onClick = { ( ) => setIsMobileOpen ( true ) }
120- size = "icon"
121- variant = "ghost"
122- >
123- < ListIcon className = "h-5 w-5" weight = "duotone" />
124- </ Button >
125-
126- < div className = "flex items-center gap-3" >
127- < Logo />
128- </ div >
129- </ div >
130-
131- < div className = "ml-auto flex items-center gap-2" >
132- < ThemeToggle />
133-
134- < Button
135- aria-label = "Help"
136- className = "hidden h-8 w-8 md:flex"
137- size = "icon"
138- variant = "ghost"
139- >
140- < InfoIcon className = "h-6 w-6" weight = "duotone" />
141- </ Button >
142-
143- < NotificationsPopover />
144- < SignOutButton />
145- </ div >
146- </ div >
147- </ header >
148-
149- { isMobileOpen && (
150- < div
151- aria-hidden = "true"
152- className = "fixed inset-0 z-30 bg-black/20 md:hidden"
153- onClick = { closeSidebar }
154- />
155- ) }
156-
157- < aside
158- aria-label = "Demo navigation"
159- className = { cn (
160- 'fixed inset-y-0 left-0 z-40 w-64 bg-background' ,
161- 'border-r pt-16 transition-transform duration-200 ease-out md:translate-x-0' ,
162- isMobileOpen ? 'translate-x-0' : '-translate-x-full'
163- ) }
164- >
165- < Button
166- aria-label = "Close sidebar"
167- className = "absolute top-3 right-3 z-50 h-8 w-8 p-0 md:hidden"
168- onClick = { closeSidebar }
169- size = "sm"
170- variant = "ghost"
171- >
172- < XIcon className = "h-4 w-4" weight = "duotone" />
173- </ Button >
174-
175- < ScrollArea className = "h-[calc(100vh-4rem)]" >
176- < nav className = "space-y-4 p-3" >
177- < div className = "flex items-center gap-3 rounded border bg-muted/50 p-3" >
178- < div className = "rounded border border-primary/20 bg-primary/10 p-2" >
179- < GlobeIcon
180- aria-hidden = "true"
181- className = "h-5 w-5 text-primary"
182- weight = "duotone"
183- />
184- </ div >
185- < div className = "min-w-0 flex-1" >
186- < h2 className = "truncate font-semibold text-sm" > Landing Page</ h2 >
187- < Link
188- className = "truncate text-muted-foreground text-xs"
189- href = { DEMO_WEBSITE_URL }
190- rel = "noopener"
191- target = "_blank"
192- >
193- www.databuddy.cc
194- </ Link >
195- </ div >
196- </ div >
197-
198- { demoNavigation . map ( ( section ) => (
199- < div key = { section . title } >
200- < h3 className = "mb-2 px-2 font-semibold text-muted-foreground text-xs uppercase tracking-wider" >
201- { section . title }
202- </ h3 >
203- < ul className = "ml-1 space-y-1" >
204- { section . items . map ( ( item ) => {
205- const isActive = pathname === item . href ;
206- const Icon = item . icon ;
207-
208- return (
209- < li key = { item . name } >
210- < Link
211- className = { cn (
212- 'flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-sm transition-all' ,
213- isActive
214- ? 'bg-primary/15 font-medium text-primary'
215- : 'text-foreground hover:bg-accent/70'
216- ) }
217- href = { item . href }
218- >
219- < Icon
220- aria-hidden = "true"
221- className = { cn (
222- 'h-4 w-4' ,
223- isActive && 'text-primary'
224- ) }
225- weight = "duotone"
226- />
227- < span className = "truncate" > { item . name } </ span >
228- </ Link >
229- </ li >
230- ) ;
231- } ) }
232- </ ul >
233- </ div >
234- ) ) }
235- </ nav >
236- </ ScrollArea >
237- </ aside >
238- </ >
239- ) ;
240- }
24110
24211interface MainLayoutProps {
24312 children : React . ReactNode ;
@@ -260,20 +29,27 @@ export default function MainLayout({ children }: MainLayoutProps) {
26029 } ;
26130
26231 return (
263- < div className = "h-screen overflow-hidden bg-gradient-to-br from-background to-muted/20 text-foreground" >
32+ < div className = "h-screen overflow-hidden text-foreground" >
26433 < Sidebar />
265- < main className = "relative h-screen pt-16 md:pl-64" >
266- < div className = "h-[calc(100vh-4rem)] overflow-y-scroll" >
267- < div className = "mx-auto max-w-[1600px] p-3 sm:p-4 lg:p-6" >
268- < AnalyticsToolbar
269- isRefreshing = { isRefreshing }
270- onRefresh = { handleRefresh }
271- />
34+ < div className = "relative h-screen pl-0 md:pl-84" >
35+ < div className = "h-screen overflow-y-auto overflow-x-hidden pt-16 md:pt-0" >
36+ < div
37+ className = { cn (
38+ 'mx-auto flex h-full max-w-[1600px] flex-col' ,
39+ 'p-3 sm:p-4 lg:p-6'
40+ ) }
41+ >
42+ < div className = "flex-shrink-0 space-y-4" >
43+ < AnalyticsToolbar
44+ isRefreshing = { isRefreshing }
45+ onRefresh = { handleRefresh }
46+ />
47+ </ div >
27248
273- { children }
49+ < div className = "min-h-0 flex-1" > { children } </ div >
27450 </ div >
27551 </ div >
276- </ main >
52+ </ div >
27753 </ div >
27854 ) ;
27955}
0 commit comments