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