Skip to content

Commit 04a0c19

Browse files
committed
ui: revamp the sidebar layout
1 parent da18414 commit 04a0c19

1 file changed

Lines changed: 277 additions & 41 deletions

File tree

apps/web/src/components/dashboard/Sidebar.tsx

Lines changed: 277 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"use client";
22

3-
import React, { useState } from "react";
3+
import React, { useState, useEffect } from "react";
44
import Link from "next/link";
55
import SidebarItem from "../sidebar/SidebarItem";
6-
import { useRouter } from "next/navigation";
6+
import { useRouter, usePathname } from "next/navigation";
77
import { IconWrapper } from "../ui/IconWrapper";
88
import { motion, AnimatePresence } from "framer-motion";
99
import {
@@ -16,6 +16,9 @@ import {
1616
DocumentTextIcon,
1717
Cog6ToothIcon,
1818
NewspaperIcon,
19+
Squares2X2Icon,
20+
ChevronDownIcon,
21+
LockClosedIcon,
1922
} from "@heroicons/react/24/outline";
2023
import { useShowSidebar } from "@/store/useShowSidebar";
2124
import { signOut, useSession } from "next-auth/react";
@@ -24,7 +27,15 @@ import { useSubscription } from "@/hooks/useSubscription";
2427
import { OpensoxProBadge } from "../sheet/OpensoxProBadge";
2528
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
2629

27-
const SIDEBAR_ROUTES = [
30+
type RouteConfig = {
31+
path: string;
32+
label: string;
33+
icon: React.ReactNode;
34+
badge?: string; // optional badge text (e.g., "New", "Beta")
35+
};
36+
37+
// free features only
38+
const FREE_ROUTES: RouteConfig[] = [
2839
{
2940
path: "/dashboard/home",
3041
label: "Home",
@@ -40,30 +51,55 @@ const SIDEBAR_ROUTES = [
4051
label: "OSS Sheet",
4152
icon: <DocumentTextIcon className="size-5" />,
4253
},
54+
];
55+
56+
// premium features under Opensox Pro
57+
const PREMIUM_ROUTES: RouteConfig[] = [
58+
{
59+
path: "/dashboard/pro/dashboard",
60+
label: "Dashboard",
61+
icon: <Squares2X2Icon className="size-5" />,
62+
badge: "New",
63+
},
4364
{
4465
path: "/dashboard/newsletters",
45-
label: "Newsletters",
66+
label: "Newsletter",
4667
icon: <NewspaperIcon className="size-5" />,
47-
isPro: true,
68+
badge: "New"
4869
},
4970
];
5071

5172
export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
5273
const { setShowSidebar, isCollapsed, toggleCollapsed } = useShowSidebar();
5374
const router = useRouter();
75+
const pathname = usePathname();
5476
const { isPaidUser } = useSubscription();
77+
const [proSectionExpanded, setProSectionExpanded] = useState(true);
78+
79+
// auto-expand pro section if user is on a premium route
80+
useEffect(() => {
81+
if (isPaidUser) {
82+
const isOnPremiumRoute = PREMIUM_ROUTES.some((route) => {
83+
return pathname === route.path || pathname.startsWith(`${route.path}/`);
84+
});
85+
if (isOnPremiumRoute) {
86+
setProSectionExpanded(true);
87+
}
88+
}
89+
}, [pathname, isPaidUser]);
5590

5691
const reqFeatureHandler = () => {
5792
window.open("https://github.com/apsinghdev/opensox/issues", "_blank");
5893
};
5994

60-
const proClickHandler = () => {
95+
const handleProSectionClick = () => {
6196
if (isPaidUser) {
62-
router.push("/dashboard/pro/dashboard");
97+
setProSectionExpanded(!proSectionExpanded);
6398
} else {
6499
router.push("/pricing");
65100
}
66101
};
102+
67103
const desktopWidth = isCollapsed ? 80 : 288;
68104
const mobileWidth = desktopWidth;
69105

@@ -118,60 +154,260 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
118154
</div>
119155

120156
<div className="sidebar-body flex-grow flex-col overflow-y-auto px-3 py-4 space-y-1">
121-
{SIDEBAR_ROUTES.map((route) => {
157+
{/* free features section */}
158+
{FREE_ROUTES.map((route) => {
159+
const isActive =
160+
pathname === route.path || pathname.startsWith(`${route.path}/`);
122161
return (
123162
<Link href={route.path} key={route.path}>
124-
<div className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 hover:bg-[#292929] group">
125-
<span className="shrink-0 text-[#eaeaea] group-hover:text-white transition-colors">
163+
<div
164+
className={`w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 group ${
165+
isActive
166+
? "bg-brand-purple/10 border-l-2 border-brand-purple"
167+
: "hover:bg-dash-hover"
168+
}`}
169+
>
170+
<span
171+
className={`shrink-0 transition-colors ${
172+
isActive
173+
? "text-brand-purple"
174+
: "text-text-secondary group-hover:text-text-primary"
175+
}`}
176+
>
126177
{route.icon}
127178
</span>
128179
{!isCollapsed && (
129-
<div className="flex items-center gap-1">
130-
<h1 className="text-xs font-medium text-[#c8c8c8] group-hover:text-white transition-colors">
180+
<div className="flex items-center gap-1.5 flex-1 min-w-0">
181+
<h1
182+
className={`text-xs font-medium transition-colors ${
183+
isActive
184+
? "text-text-primary"
185+
: "text-text-tertiary group-hover:text-text-primary"
186+
}`}
187+
>
131188
{route.label}
132189
</h1>
133-
{route.isPro && (
134-
<OpensoxProBadge className="px-1.5 py-0.5 scale-75" />
190+
{route.badge && (
191+
<span className="px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-brand-purple/20 text-brand-purple rounded border border-brand-purple/30 shrink-0">
192+
{route.badge}
193+
</span>
135194
)}
136195
</div>
137196
)}
138197
</div>
139198
</Link>
140199
);
141200
})}
142-
143-
144-
145-
201+
202+
{/* divider */}
203+
{!isCollapsed && (
204+
<div className="my-3 px-3">
205+
<div className="border-t border-dash-border" />
206+
</div>
207+
)}
208+
209+
{/* premium section */}
210+
{!isCollapsed ? (
211+
<div className="space-y-1">
212+
{(() => {
213+
const isPremiumRouteActive = PREMIUM_ROUTES.some(
214+
(route) =>
215+
pathname === route.path ||
216+
pathname.startsWith(`${route.path}/`)
217+
);
218+
const newFeaturesCount = PREMIUM_ROUTES.filter(
219+
(route) => route.badge
220+
).length;
221+
return (
222+
<div
223+
onClick={handleProSectionClick}
224+
className={`w-full h-[44px] flex items-center justify-between rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 group ${
225+
isPremiumRouteActive
226+
? "bg-brand-purple/10 border-l-2 border-brand-purple"
227+
: "hover:bg-dash-hover"
228+
}`}
229+
role="button"
230+
tabIndex={0}
231+
aria-expanded={proSectionExpanded}
232+
aria-label="Opensox Pro section"
233+
onKeyDown={(e) => {
234+
if (e.key === "Enter" || e.key === " ") {
235+
e.preventDefault();
236+
handleProSectionClick();
237+
}
238+
}}
239+
>
240+
<div className="flex items-center gap-3 flex-1 min-w-0">
241+
<span
242+
className={`shrink-0 transition-colors ${
243+
isPremiumRouteActive
244+
? "text-brand-purple"
245+
: "text-text-secondary group-hover:text-text-primary"
246+
}`}
247+
>
248+
<StarIcon className="size-5" />
249+
</span>
250+
<div className="flex items-center gap-1.5 flex-1 min-w-0">
251+
<h1
252+
className={`text-xs font-medium transition-colors ${
253+
isPremiumRouteActive
254+
? "text-text-primary"
255+
: "text-text-tertiary group-hover:text-text-primary"
256+
}`}
257+
>
258+
Opensox Pro
259+
</h1>
260+
<OpensoxProBadge className="px-1.5 py-0.5 scale-75 shrink-0" />
261+
{newFeaturesCount > 0 && (
262+
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-brand-purple text-text-primary rounded-full shrink-0 min-w-[18px] h-[18px] flex items-center justify-center">
263+
{newFeaturesCount}
264+
</span>
265+
)}
266+
</div>
267+
</div>
268+
{isPaidUser && (
269+
<ChevronDownIcon
270+
className={`size-4 text-text-muted transition-transform duration-300 shrink-0 ${
271+
proSectionExpanded ? "" : "-rotate-90"
272+
}`}
273+
/>
274+
)}
275+
</div>
276+
);
277+
})()}
278+
279+
{/* premium sub-items (only show if paid user and expanded) */}
280+
{isPaidUser && proSectionExpanded && (
281+
<motion.div
282+
initial={{ opacity: 0, height: 0 }}
283+
animate={{ opacity: 1, height: "auto" }}
284+
exit={{ opacity: 0, height: 0 }}
285+
transition={{ duration: 0.2 }}
286+
className="pl-8 space-y-1"
287+
>
288+
{PREMIUM_ROUTES.map((route) => {
289+
const isActive =
290+
pathname === route.path ||
291+
pathname.startsWith(`${route.path}/`);
292+
return (
293+
<Link href={route.path} key={route.path}>
294+
<div
295+
className={`w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 group ${
296+
isActive
297+
? "bg-brand-purple/10 border-l-2 border-brand-purple"
298+
: "hover:bg-dash-hover"
299+
}`}
300+
>
301+
<span
302+
className={`shrink-0 transition-colors ${
303+
isActive
304+
? "text-brand-purple"
305+
: "text-text-secondary group-hover:text-text-primary"
306+
}`}
307+
>
308+
{route.icon}
309+
</span>
310+
<div className="flex items-center gap-1.5 flex-1 min-w-0">
311+
<h1
312+
className={`text-xs font-medium transition-colors ${
313+
isActive
314+
? "text-text-primary"
315+
: "text-text-tertiary group-hover:text-text-primary"
316+
}`}
317+
>
318+
{route.label}
319+
</h1>
320+
{route.badge && (
321+
<span className="px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-brand-purple/20 text-brand-purple rounded border border-brand-purple/30 shrink-0">
322+
{route.badge}
323+
</span>
324+
)}
325+
</div>
326+
</div>
327+
</Link>
328+
);
329+
})}
330+
</motion.div>
331+
)}
332+
333+
{/* free user: show locked preview */}
334+
{!isPaidUser && (
335+
<motion.div
336+
initial={{ opacity: 0, height: 0 }}
337+
animate={{ opacity: 1, height: "auto" }}
338+
exit={{ opacity: 0, height: 0 }}
339+
transition={{ duration: 0.2 }}
340+
className="pl-8 space-y-1"
341+
>
342+
{PREMIUM_ROUTES.map((route) => (
343+
<div
344+
key={route.path}
345+
onClick={() => router.push("/pricing")}
346+
className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 opacity-50 hover:opacity-75 group"
347+
role="button"
348+
tabIndex={0}
349+
aria-label={`${route.label} - Upgrade to Pro`}
350+
onKeyDown={(e) => {
351+
if (e.key === "Enter" || e.key === " ") {
352+
e.preventDefault();
353+
router.push("/pricing");
354+
}
355+
}}
356+
>
357+
<span className="shrink-0 text-text-secondary">
358+
{route.icon}
359+
</span>
360+
<div className="flex items-center gap-1.5 flex-1 min-w-0">
361+
<h1 className="text-xs font-medium text-text-tertiary">
362+
{route.label}
363+
</h1>
364+
{route.badge && (
365+
<span className="px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-brand-purple/20 text-brand-purple rounded border border-brand-purple/30 shrink-0 opacity-75">
366+
{route.badge}
367+
</span>
368+
)}
369+
</div>
370+
<div className="ml-auto">
371+
<LockClosedIcon className="size-3 text-text-muted" />
372+
</div>
373+
</div>
374+
))}
375+
</motion.div>
376+
)}
377+
</div>
378+
) : (
379+
// collapsed sidebar: show icon only
380+
<div
381+
onClick={handleProSectionClick}
382+
className="w-full h-[44px] flex items-center justify-center rounded-md cursor-pointer transition-colors hover:bg-dash-hover group"
383+
role="button"
384+
tabIndex={0}
385+
aria-label="Opensox Pro"
386+
onKeyDown={(e) => {
387+
if (e.key === "Enter" || e.key === " ") {
388+
e.preventDefault();
389+
handleProSectionClick();
390+
}
391+
}}
392+
>
393+
<StarIcon className="size-5 text-text-secondary group-hover:text-text-primary transition-colors" />
394+
</div>
395+
)}
396+
397+
{/* divider */}
398+
{!isCollapsed && (
399+
<div className="my-3 px-3">
400+
<div className="border-t border-dash-border" />
401+
</div>
402+
)}
403+
404+
{/* utility features */}
146405
<SidebarItem
147406
itemName="Request a feature"
148407
onclick={reqFeatureHandler}
149408
icon={<SparklesIcon className="size-5" />}
150409
collapsed={isCollapsed}
151410
/>
152-
{!isCollapsed && !isPaidUser ? (
153-
<div
154-
className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 hover:bg-dash-hover group"
155-
onClick={proClickHandler}
156-
>
157-
<span className="shrink-0 text-text-secondary group-hover:text-text-primary transition-colors">
158-
<StarIcon className="size-5" />
159-
</span>
160-
<div className="flex items-center gap-1">
161-
<h1 className="text-xs font-medium text-text-tertiary group-hover:text-text-primary transition-colors">
162-
Opensox Pro
163-
</h1>
164-
<OpensoxProBadge className="px-1.5 py-0.5 scale-75" />
165-
</div>
166-
</div>
167-
) : (
168-
<SidebarItem
169-
itemName="Opensox Pro"
170-
onclick={proClickHandler}
171-
icon={<StarIcon className="size-5" />}
172-
collapsed={isCollapsed}
173-
/>
174-
)}
175411
</div>
176412

177413
{/* Bottom profile */}

0 commit comments

Comments
 (0)