Skip to content

Commit 857d40e

Browse files
authored
feat(layout): add responsive sidebar navigation (#468)
1 parent f84f094 commit 857d40e

18 files changed

Lines changed: 805 additions & 225 deletions

src/App.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33

44
import {
55
AppHeader,
6+
AppSidebar,
67
NoteDrawer,
78
InfinityLoader,
89
OccurrenceDrawer,
@@ -47,9 +48,12 @@ const App = () => {
4748
<NoteDrawer />
4849
<OccurrenceDrawer />
4950
<ConfirmationDialog />
50-
<main className="flex h-full flex-1 flex-col items-start bg-white max-md:pb-11.25 dark:bg-black">
51-
<AppRoutes />
52-
</main>
51+
<div className="flex w-full flex-1 items-stretch">
52+
<AppSidebar />
53+
<main className="flex h-full min-w-0 flex-1 flex-col items-start bg-white max-md:pb-11.25 dark:bg-black">
54+
<AppRoutes />
55+
</main>
56+
</div>
5357
</>
5458
);
5559
};

src/Routes.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React from 'react';
2-
import { Route, Routes, Navigate } from 'react-router';
2+
import { Route, Routes, Navigate, useLocation } from 'react-router';
33

44
import {
55
LoginPage,
66
NotesPage,
77
HabitsPage,
8-
AccountPage,
98
RegisterPage,
9+
SettingsPage,
1010
DayCalendarPage,
1111
OAuthConsentPage,
1212
HabitDetailsPage,
@@ -16,6 +16,12 @@ import {
1616
AnonymousLoginPage,
1717
} from '@pages';
1818

19+
const AccountRedirect = () => {
20+
const { hash, search } = useLocation();
21+
22+
return <Navigate replace to={`/settings${search}${hash}`} />;
23+
};
24+
1925
const AppRoutes = () => {
2026
return (
2127
<Routes>
@@ -34,7 +40,8 @@ const AppRoutes = () => {
3440
<Route path="/habits" element={<HabitsPage />} />
3541
<Route path="/habits/:habitId" element={<HabitDetailsPage />} />
3642
<Route path="/notes" element={<NotesPage />} />
37-
<Route path="/account" element={<AccountPage />} />
43+
<Route path="/settings" element={<SettingsPage />} />
44+
<Route path="/account" element={<AccountRedirect />} />
3845
<Route path="/login" element={<LoginPage />} />
3946
<Route path="/anonymous-login" element={<AnonymousLoginPage />} />
4047
<Route path="/register" element={<RegisterPage />} />

src/components/header/Header.tsx

Lines changed: 49 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,25 @@
1-
import { Label, Dropdown, Separator } from '@heroui/react';
21
import { today, getLocalTimeZone } from '@internationalized/date';
32
import {
43
ListIcon,
5-
NoteIcon,
6-
UserIcon,
7-
RepeatIcon,
8-
MapPinIcon,
9-
SignOutIcon,
10-
GithubLogoIcon,
114
NotePencilIcon,
12-
RoadHorizonIcon,
13-
CalendarDotsIcon,
145
CalendarCheckIcon,
15-
ArrowSquareOutIcon,
166
} from '@phosphor-icons/react';
17-
import { useLocation, useNavigate } from 'react-router';
187

19-
import { CustomKbd, CustomButton } from '@components';
8+
import { UserMenu, CustomKbd, CustomButton } from '@components';
209
import { useScreenWidth } from '@hooks';
21-
import { signOut } from '@services';
2210
import {
2311
useUser,
2412
useNoteDrawerActions,
13+
useMobileSidebarActions,
2514
useOccurrenceDrawerActions,
2615
} from '@stores';
2716

2817
import ThemeToggle from './ThemeToggle';
2918

30-
const ROADMAP_URL = 'https://habitrack.featurebase.app/roadmap';
31-
const GITHUB_URL = 'https://github.com/domhhv/habitrack';
32-
3319
const Header = () => {
3420
const user = useUser();
35-
const navigate = useNavigate();
3621
const { isMobile } = useScreenWidth();
37-
const { pathname } = useLocation();
22+
const { openMobileSidebar } = useMobileSidebarActions();
3823
const { openNoteDrawer } = useNoteDrawerActions();
3924
const { openOccurrenceDrawer } = useOccurrenceDrawerActions();
4025

@@ -48,178 +33,23 @@ const Header = () => {
4833
});
4934
};
5035

51-
const navItems = [
52-
{
53-
href: '/calendar/month',
54-
icon: CalendarDotsIcon,
55-
id: 'calendar',
56-
isActive: pathname.startsWith('/calendar'),
57-
label: 'Calendar',
58-
},
59-
...(user
60-
? ([
61-
{
62-
href: '/habits',
63-
icon: RepeatIcon,
64-
id: 'habits',
65-
isActive: pathname.startsWith('/habits'),
66-
label: 'Habits',
67-
},
68-
{
69-
href: '/notes',
70-
icon: NoteIcon,
71-
id: 'notes',
72-
isActive: pathname === '/notes',
73-
label: 'Notes',
74-
},
75-
{
76-
href: '/account',
77-
icon: UserIcon,
78-
id: 'account',
79-
isActive: pathname === '/account',
80-
label: 'Account',
81-
},
82-
] as const)
83-
: []),
84-
] as const;
85-
86-
const activeLabel =
87-
navItems.find((item) => {
88-
return item.isActive;
89-
})?.label ?? 'Menu';
90-
9136
return (
92-
<nav className="bg-background border-border sticky top-0 z-50 h-16 w-full border-b">
93-
<header className="flex h-full w-full items-center justify-between px-8 lg:px-8">
94-
<div className="flex items-center gap-2">
95-
<Dropdown>
96-
<CustomButton
97-
size="sm"
98-
variant="outline"
99-
aria-label="Open navigation menu"
100-
>
101-
<ListIcon size={isMobile ? 16 : 18} />
102-
<span>{activeLabel}</span>
103-
</CustomButton>
104-
<Dropdown.Popover className="min-w-[250px]">
105-
<Dropdown.Menu aria-label="Navigation">
106-
<Dropdown.Section>
107-
{navItems.map(({ href, icon: Icon, id, isActive, label }) => {
108-
return (
109-
<Dropdown.Item
110-
id={id}
111-
key={id}
112-
href={href}
113-
textValue={label}
114-
className="justify-between"
115-
>
116-
<div className="flex items-center gap-2">
117-
<Icon className="size-4 shrink-0" />
118-
<Label>{label}</Label>
119-
</div>
120-
{isActive && user && (
121-
<div className="text-muted flex items-center gap-1 text-xs">
122-
<MapPinIcon />
123-
<span className="ms-auto">You&apos;re here</span>
124-
</div>
125-
)}
126-
</Dropdown.Item>
127-
);
128-
})}
129-
</Dropdown.Section>
130-
<Separator className="bg-muted/25" />
131-
<Dropdown.Section>
132-
<Dropdown.Item
133-
id="roadmap"
134-
target="_blank"
135-
href={ROADMAP_URL}
136-
textValue="Roadmap"
137-
rel="noopener noreferrer"
138-
className="justify-between"
139-
>
140-
<div className="flex items-center gap-2">
141-
<RoadHorizonIcon className="size-4 shrink-0" />
142-
<Label>Roadmap</Label>
143-
</div>
144-
<ArrowSquareOutIcon className="text-muted/50 size-4 shrink-0" />
145-
</Dropdown.Item>
146-
<Dropdown.Item
147-
target="_blank"
148-
id="source-code"
149-
href={GITHUB_URL}
150-
rel="noopener noreferrer"
151-
className="justify-between"
152-
textValue="Source Code on GitHub"
153-
>
154-
<div className="flex items-center gap-2">
155-
<GithubLogoIcon className="size-4 shrink-0" />
156-
<Label>Source Code on GitHub</Label>
157-
</div>
158-
<ArrowSquareOutIcon className="text-muted/50 size-4 shrink-0 justify-self-end" />
159-
</Dropdown.Item>
160-
</Dropdown.Section>
161-
{!!user && (
162-
<>
163-
<Separator className="bg-muted/25" />
164-
<Dropdown.Section>
165-
<Dropdown.Item
166-
id="logout"
167-
variant="danger"
168-
onAction={signOut}
169-
textValue="Log Out"
170-
>
171-
<SignOutIcon className="text-danger size-4 shrink-0" />
172-
<Label>Log Out</Label>
173-
</Dropdown.Item>
174-
</Dropdown.Section>
175-
</>
176-
)}
177-
</Dropdown.Menu>
178-
</Dropdown.Popover>
179-
</Dropdown>
180-
</div>
37+
<nav className="bg-background border-border sticky top-0 z-50 h-16 w-full border-b lg:hidden">
38+
<header className="flex h-full w-full items-center justify-between px-4 md:px-8">
39+
<CustomButton
40+
size="sm"
41+
isIconOnly
42+
variant="outline"
43+
onPress={openMobileSidebar}
44+
aria-label="Open navigation menu"
45+
>
46+
<ListIcon size={isMobile ? 16 : 18} />
47+
</CustomButton>
18148
<div className="flex items-center gap-2">
18249
<ThemeToggle />
183-
{!!user && (
184-
<div className="bg-background-secondary border-border dark:border-border fixed right-0 bottom-0 left-0 z-50 flex w-full gap-2 border-t p-2 px-4 md:right-4 md:bottom-4 md:left-auto md:w-auto md:border-0 md:bg-transparent md:p-0 xl:static xl:right-auto xl:bottom-auto xl:border-0 xl:bg-transparent xl:p-0 dark:md:bg-transparent dark:xl:bg-transparent">
185-
<CustomButton
186-
size="sm"
187-
variant="bordered"
188-
fullWidth={isMobile}
189-
onPress={dispatchNoteDrawerOpen}
190-
className="flex-1 max-md:h-8 max-md:gap-1 max-md:rounded-full md:flex-none"
191-
>
192-
<NotePencilIcon size={isMobile ? 12 : 16} />
193-
Note
194-
<CustomKbd
195-
isSolid
196-
size="sm"
197-
variant="light"
198-
shortcutParams={['n', dispatchNoteDrawerOpen]}
199-
>
200-
N
201-
</CustomKbd>
202-
</CustomButton>
203-
<CustomButton
204-
size="sm"
205-
variant="primary"
206-
fullWidth={isMobile}
207-
onPress={dispatchOccurrenceDrawerOpen}
208-
className="flex-1 max-md:h-8 max-md:gap-1 max-md:rounded-full md:flex-none"
209-
>
210-
<CalendarCheckIcon size={isMobile ? 12 : 16} />
211-
Log
212-
<CustomKbd
213-
size="sm"
214-
variant="default"
215-
shortcutParams={['l', dispatchOccurrenceDrawerOpen]}
216-
>
217-
L
218-
</CustomKbd>
219-
</CustomButton>
220-
</div>
221-
)}
222-
{!user && (
50+
{user ? (
51+
<UserMenu />
52+
) : (
22353
<>
22454
<CustomButton
22555
size="sm"
@@ -228,16 +58,7 @@ const Header = () => {
22858
data-testid="login-button"
22959
>
23060
Log In
231-
<CustomKbd
232-
size="sm"
233-
variant="default"
234-
shortcutParams={[
235-
'i',
236-
() => {
237-
navigate('/login');
238-
},
239-
]}
240-
>
61+
<CustomKbd size="sm" variant="default">
24162
I
24263
</CustomKbd>
24364
</CustomButton>
@@ -248,23 +69,44 @@ const Header = () => {
24869
data-testid="register-button"
24970
>
25071
Join
251-
<CustomKbd
252-
size="sm"
253-
variant="default"
254-
shortcutParams={[
255-
'j',
256-
() => {
257-
navigate('/register');
258-
},
259-
]}
260-
>
72+
<CustomKbd size="sm" variant="default">
26173
J
26274
</CustomKbd>
26375
</CustomButton>
26476
</>
26577
)}
26678
</div>
26779
</header>
80+
{!!user && (
81+
<div className="bg-background-secondary border-border dark:border-border fixed right-0 bottom-0 left-0 z-50 flex w-full gap-2 border-t p-2 px-4 md:right-4 md:bottom-4 md:left-auto md:w-auto md:border-0 md:bg-transparent md:p-0 dark:md:bg-transparent">
82+
<CustomButton
83+
size="sm"
84+
variant="bordered"
85+
fullWidth={isMobile}
86+
onPress={dispatchNoteDrawerOpen}
87+
className="flex-1 max-md:h-8 max-md:gap-1 max-md:rounded-full md:flex-none"
88+
>
89+
<NotePencilIcon size={isMobile ? 12 : 16} />
90+
Note
91+
<CustomKbd isSolid size="sm" variant="light">
92+
N
93+
</CustomKbd>
94+
</CustomButton>
95+
<CustomButton
96+
size="sm"
97+
variant="primary"
98+
fullWidth={isMobile}
99+
onPress={dispatchOccurrenceDrawerOpen}
100+
className="flex-1 max-md:h-8 max-md:gap-1 max-md:rounded-full md:flex-none"
101+
>
102+
<CalendarCheckIcon size={isMobile ? 12 : 16} />
103+
Log
104+
<CustomKbd size="sm" variant="default">
105+
L
106+
</CustomKbd>
107+
</CustomButton>
108+
</div>
109+
)}
268110
</nav>
269111
);
270112
};

src/components/header/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default as AppHeader } from './Header';
2+
export { default as ThemeToggle } from './ThemeToggle';

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export * from './header';
66
export * from './metric';
77
export * from './note';
88
export * from './occurrence';
9+
export * from './sidebar';
910
export * from './stock';
1011
export * from './trait';

0 commit comments

Comments
 (0)