@@ -6,169 +6,145 @@ import { useState, useEffect } from 'react'
66import LanguageSwitcher from './LanguageSwitcher'
77import { ThemeToggle } from '../theme-provider'
88
9- const NAV_GROUPS = [
10- {
11- label : 'SISTEMA' ,
12- links : [
13- { href : '/dashboard' , label : 'Dashboard' } ,
14- { href : '/deploy' , label : 'Deploy' } ,
15- { href : '/gateway' , label : 'Gateway' } ,
16- { href : '/status' , label : 'Stato' } ,
17- ] ,
18- } ,
19- {
20- label : 'JOB HUNTING' ,
21- links : [
22- { href : '/jobs' , label : 'Offerte' } ,
23- { href : '/applications' , label : 'Candidature' } ,
24- { href : '/interviews' , label : 'Colloqui' } ,
25- { href : '/companies' , label : 'Aziende' } ,
26- { href : '/cover-letters' , label : 'Cover Letter' } ,
27- { href : '/profiles' , label : 'Profili' } ,
28- { href : '/alerts' , label : 'Alert' } ,
29- ] ,
30- } ,
31- {
32- label : 'AGENTI' ,
33- links : [
34- { href : '/agents' , label : 'Agenti' } ,
35- { href : '/assistant' , label : 'Assistente' } ,
36- { href : '/tasks' , label : 'Task' } ,
37- { href : '/queue' , label : 'Queue' } ,
38- { href : '/workers' , label : 'Workers' } ,
39- ] ,
40- } ,
41- {
42- label : 'DATI' ,
43- links : [
44- { href : '/events' , label : 'Events' } ,
45- { href : '/history' , label : 'History' } ,
46- { href : '/analytics' , label : 'Analytics' } ,
47- { href : '/logs' , label : 'Logs' } ,
48- { href : '/database' , label : 'Database' } ,
49- ] ,
50- } ,
51- {
52- label : 'TOOLS' ,
53- links : [
54- { href : '/api-explorer' , label : 'API Explorer' } ,
55- { href : '/automations' , label : 'Automazioni' } ,
56- { href : '/scheduler' , label : 'Scheduler' } ,
57- { href : '/monitoring' , label : 'Monitoring' } ,
58- { href : '/errors' , label : 'Errori' } ,
59- { href : '/performance' , label : 'Performance' } ,
60- { href : '/git' , label : 'Git' } ,
61- ] ,
62- } ,
63- {
64- label : 'CONFIG' ,
65- links : [
66- { href : '/providers' , label : 'Provider' } ,
67- { href : '/rate-limiter' , label : 'Rate Limiter' } ,
68- { href : '/credentials' , label : 'Credenziali' } ,
69- { href : '/channels' , label : 'Canali' } ,
70- { href : '/plugins' , label : 'Plugin' } ,
71- { href : '/templates' , label : 'Template' } ,
72- { href : '/memory' , label : 'Memory' } ,
73- { href : '/notifications' , label : 'Notifiche' } ,
74- { href : '/settings' , label : 'Impostazioni' } ,
75- { href : '/cron' , label : 'Cron' } ,
76- ] ,
77- } ,
78- ]
9+ // ── Nav data: [group, [[href, label, badge?][]]] ───────────────────────────
7910
80- const PROTECTED_PREFIXES = [
81- '/dashboard' , '/profile' , '/capitano' , '/scout' , '/analista' ,
82- '/scorer' , '/scrittore' , '/critico' , '/sentinella' , '/team' ,
83- '/applications' , '/positions' , '/ready' , '/risposte' , '/crescita' ,
84- '/assistente' , '/setup' ,
11+ const NAV : [ string , [ string , string , number ?] [ ] ] [ ] = [
12+ [ 'SISTEMA' , [ [ '/dashboard' , 'Dashboard' ] , [ '/deploy' , 'Deploy' ] , [ '/gateway' , 'Gateway' ] , [ '/status' , 'Stato' ] ] ] ,
13+ [ 'JOB HUNTING' , [ [ '/jobs' , 'Offerte' ] , [ '/applications' , 'Candidature' , 3 ] , [ '/interviews' , 'Colloqui' , 1 ] , [ '/companies' , 'Aziende' ] , [ '/cover-letters' , 'Cover Letter' ] , [ '/profiles' , 'Profili' ] , [ '/alerts' , 'Alert' , 5 ] ] ] ,
14+ [ 'AGENTI' , [ [ '/agents' , 'Agenti' ] , [ '/assistant' , 'Assistente' ] , [ '/tasks' , 'Task' ] , [ '/queue' , 'Queue' ] , [ '/workers' , 'Workers' ] ] ] ,
15+ [ 'DATI' , [ [ '/events' , 'Events' ] , [ '/history' , 'History' ] , [ '/analytics' , 'Analytics' ] , [ '/logs' , 'Logs' ] , [ '/database' , 'Database' ] ] ] ,
16+ [ 'TOOLS' , [ [ '/api-explorer' , 'API Explorer' ] , [ '/automations' , 'Automazioni' ] , [ '/scheduler' , 'Scheduler' ] , [ '/monitoring' , 'Monitoring' ] , [ '/errors' , 'Errori' ] , [ '/performance' , 'Performance' ] , [ '/git' , 'Git' ] ] ] ,
17+ [ 'CONFIG' , [ [ '/providers' , 'Provider' ] , [ '/rate-limiter' , 'Rate Limiter' ] , [ '/credentials' , 'Credenziali' ] , [ '/channels' , 'Canali' ] , [ '/plugins' , 'Plugin' ] , [ '/templates' , 'Template' ] , [ '/memory' , 'Memory' ] , [ '/notifications' , 'Notifiche' ] , [ '/settings' , 'Impostazioni' ] , [ '/cron' , 'Cron' ] ] ] ,
8518]
8619
20+ const PROTECTED = [ '/dashboard' , '/profile' , '/capitano' , '/scout' , '/analista' , '/scorer' , '/scrittore' , '/critico' , '/sentinella' , '/team' , '/applications' , '/positions' , '/ready' , '/risposte' , '/crescita' , '/assistente' , '/setup' ]
21+
8722export default function Sidebar ( ) {
88- const pathname = usePathname ( )
23+ const pathname = usePathname ( )
8924 const [ mobileOpen , setMobileOpen ] = useState ( false )
90- const [ isMobile , setIsMobile ] = useState ( false )
25+ const [ isMobile , setIsMobile ] = useState ( false )
26+ const [ collapsed , setCollapsed ] = useState ( false )
27+ const [ favs , setFavs ] = useState < string [ ] > ( [ ] )
28+ const [ hovered , setHovered ] = useState < string | null > ( null )
9129
92- const isProtected = pathname === '/' || PROTECTED_PREFIXES . some ( p => pathname === p || pathname . startsWith ( p + '/' ) )
30+ const isProtected = pathname === '/' || PROTECTED . some ( p => pathname === p || pathname . startsWith ( p + '/' ) )
9331
9432 useEffect ( ( ) => {
9533 const check = ( ) => setIsMobile ( window . innerWidth < 768 )
96- check ( )
97- window . addEventListener ( 'resize' , check )
34+ check ( ) ; window . addEventListener ( 'resize' , check )
9835 return ( ) => window . removeEventListener ( 'resize' , check )
9936 } , [ ] )
10037
101- // Chiudi drawer al cambio pagina
10238 useEffect ( ( ) => { setMobileOpen ( false ) } , [ pathname ] )
10339
40+ useEffect ( ( ) => {
41+ try {
42+ setFavs ( JSON . parse ( localStorage . getItem ( 'jht:sb-favs' ) ?? '[]' ) )
43+ setCollapsed ( localStorage . getItem ( 'jht:sb-coll' ) === 'true' )
44+ } catch { }
45+ } , [ ] )
46+
47+ const w = collapsed ? 48 : 200
48+
49+ useEffect ( ( ) => {
50+ document . documentElement . style . setProperty ( '--sidebar-w' , `${ w } px` )
51+ } , [ w ] )
52+
10453 if ( isProtected ) return null
10554
55+ const toggleCollapse = ( ) => {
56+ const next = ! collapsed ; setCollapsed ( next )
57+ localStorage . setItem ( 'jht:sb-coll' , String ( next ) )
58+ }
59+ const toggleFav = ( href : string ) => {
60+ const next = favs . includes ( href ) ? favs . filter ( f => f !== href ) : [ ...favs , href ]
61+ setFavs ( next ) ; localStorage . setItem ( 'jht:sb-favs' , JSON . stringify ( next ) )
62+ }
63+
64+ const allLinks = NAV . flatMap ( ( [ , ls ] ) => ls )
65+ const favLinks = allLinks . filter ( ( [ h ] ) => favs . includes ( h ) )
66+
67+ const renderLink = ( [ href , label , badge ] : [ string , string , number ?] ) => {
68+ const active = pathname === href || pathname . startsWith ( href + '/' )
69+ const isFav = favs . includes ( href )
70+ return (
71+ < li key = { href } className = "relative" onMouseEnter = { ( ) => setHovered ( href ) } onMouseLeave = { ( ) => setHovered ( null ) } >
72+ < Link href = { href } aria-current = { active ? 'page' : undefined }
73+ className = "flex items-center gap-2 px-2 py-1.5 rounded text-[11px] no-underline transition-colors"
74+ style = { { color : active ? 'var(--color-green)' : 'var(--color-muted)' , background : active ? 'rgba(0,232,122,0.08)' : 'transparent' , borderLeft : active ? '2px solid var(--color-green)' : '2px solid transparent' } } >
75+ { collapsed
76+ ? < span className = "w-full text-center text-[10px] font-semibold" title = { label } > { label [ 0 ] } </ span >
77+ : < > < span className = "flex-1 truncate" > { label } </ span >
78+ { badge ? < span className = "text-[8px] font-bold px-1 rounded-full" style = { { background : 'var(--color-red)22' , color : 'var(--color-red)' } } > { badge } </ span > : null } </ >
79+ }
80+ </ Link >
81+ { ! collapsed && hovered === href && (
82+ < button onClick = { ( ) => toggleFav ( href ) }
83+ className = "absolute right-1 top-1/2 -translate-y-1/2 text-[10px] transition-opacity"
84+ style = { { color : isFav ? 'var(--color-yellow)' : 'var(--color-dim)' , background : 'none' , border : 'none' , cursor : 'pointer' } } >
85+ { isFav ? '★' : '☆' }
86+ </ button >
87+ ) }
88+ </ li >
89+ )
90+ }
91+
10692 const sidebarContent = (
107- < aside
108- aria-label = "sidebar"
109- className = "flex flex-col overflow-y-auto h-full"
110- style = { { width : 200 , background : 'var(--color-deep)' , borderRight : '1px solid var(--color-border)' } } >
111- < div className = "px-5 py-5 border-b flex items-center justify-between" style = { { borderColor : 'var(--color-border)' } } >
112- < div >
113- < p className = "text-[11px] font-bold tracking-widest text-[var(--color-white)]" > JHT</ p >
114- < p className = "text-[9px] text-[var(--color-dim)]" > Job Hunter Team</ p >
93+ < aside role = "navigation" aria-label = "Navigazione principale"
94+ className = "flex flex-col h-full overflow-y-auto"
95+ style = { { width : w , minWidth : w , background : 'var(--color-deep)' , borderRight : '1px solid var(--color-border)' , transition : 'width 0.2s ease, min-width 0.2s ease' } } >
96+
97+ { /* Header */ }
98+ < div className = "px-3 py-4 border-b flex items-center justify-between" style = { { borderColor : 'var(--color-border)' } } >
99+ { ! collapsed && < div > < p className = "text-[11px] font-bold tracking-widest" style = { { color : 'var(--color-white)' } } > JHT</ p > < p className = "text-[9px]" style = { { color : 'var(--color-dim)' } } > Job Hunter Team</ p > </ div > }
100+ < div className = "flex items-center gap-1 ml-auto" >
101+ { isMobile && < button onClick = { ( ) => setMobileOpen ( false ) } style = { { background : 'none' , border : 'none' , color : 'var(--color-dim)' , cursor : 'pointer' , fontSize : 18 } } > ×</ button > }
102+ { ! isMobile && < button onClick = { toggleCollapse } style = { { background : 'none' , border : 'none' , color : 'var(--color-dim)' , cursor : 'pointer' , fontSize : 13 , padding : '2px 4px' } } > { collapsed ? '→' : '←' } </ button > }
115103 </ div >
116- { isMobile && (
117- < button onClick = { ( ) => setMobileOpen ( false ) } style = { { background : 'none' , border : 'none' , color : 'var(--color-dim)' , cursor : 'pointer' , fontSize : 18 } } > ×</ button >
118- ) }
119104 </ div >
120- < nav className = "flex-1 px-3 py-4 flex flex-col gap-4" >
121- { NAV_GROUPS . map ( group => (
122- < div key = { group . label } >
123- < p className = "text-[8px] font-bold tracking-widest px-2 mb-1" style = { { color : 'var(--color-dim)' } } > { group . label } </ p >
124- < ul className = "flex flex-col gap-0.5" >
125- { group . links . map ( ( { href, label } ) => {
126- const active = pathname === href || pathname . startsWith ( href + '/' )
127- return (
128- < li key = { href } >
129- < Link href = { href } aria-current = { active ? 'page' : undefined }
130- className = "block px-2 py-1.5 rounded text-[11px] no-underline transition-colors"
131- style = { { color : active ? 'var(--color-green)' : 'var(--color-muted)' , background : active ? 'rgba(0,232,122,0.08)' : 'transparent' , borderLeft : active ? '2px solid var(--color-green)' : '2px solid transparent' } } >
132- { label }
133- </ Link >
134- </ li >
135- )
136- } ) }
137- </ ul >
105+
106+ { /* Nav */ }
107+ < nav className = "flex-1 px-2 py-3 flex flex-col gap-3 overflow-y-auto" >
108+ { ! collapsed && favLinks . length > 0 && (
109+ < div >
110+ < p className = "text-[8px] font-bold tracking-widest px-2 mb-1" style = { { color : 'var(--color-yellow)' } } > ★ PREFERITI</ p >
111+ < ul className = "flex flex-col gap-0.5" > { favLinks . map ( renderLink ) } </ ul >
112+ </ div >
113+ ) }
114+ { NAV . map ( ( [ group , links ] ) => (
115+ < div key = { group } >
116+ { ! collapsed && < p className = "text-[8px] font-bold tracking-widest px-2 mb-1" style = { { color : 'var(--color-dim)' } } > { group } </ p > }
117+ < ul className = "flex flex-col gap-0.5" > { links . map ( renderLink ) } </ ul >
138118 </ div >
139119 ) ) }
140120 </ nav >
141- < div className = "px-3 py-3 border-t flex items-center justify-between" style = { { borderColor : 'var(--color-border)' } } >
142- < LanguageSwitcher />
143- < ThemeToggle />
121+
122+ { /* Footer */ }
123+ < div className = "px-3 py-3 border-t flex flex-col gap-2" style = { { borderColor : 'var(--color-border)' } } >
124+ { ! collapsed && (
125+ < div className = "flex items-center justify-between" >
126+ < span className = "text-[8px] font-mono" style = { { color : 'var(--color-dim)' } } > v1.0.0</ span >
127+ < a href = "https://github.com/leopu00/job-hunter-team" target = "_blank" rel = "noreferrer"
128+ className = "text-[8px] hover:opacity-80 transition-opacity" style = { { color : 'var(--color-dim)' } } > docs →</ a >
129+ </ div >
130+ ) }
131+ { ! collapsed && < div className = "flex items-center justify-between" > < LanguageSwitcher /> < ThemeToggle /> </ div > }
144132 </ div >
145133 </ aside >
146134 )
147135
148136 if ( isMobile ) return (
149137 < >
150- { /* Hamburger */ }
151- < button
152- onClick = { ( ) => setMobileOpen ( true ) }
153- aria-label = "Apri menu"
154- style = { { position : 'fixed' , top : 12 , left : 12 , zIndex : 60 , background : 'var(--color-panel)' , border : '1px solid var(--color-border)' , borderRadius : 6 , padding : '6px 8px' , cursor : 'pointer' , color : 'var(--color-muted)' , lineHeight : 1 } } >
155- ☰
156- </ button >
157- { /* Overlay */ }
138+ < button onClick = { ( ) => setMobileOpen ( true ) } aria-label = "Apri menu"
139+ style = { { position : 'fixed' , top : 12 , left : 12 , zIndex : 60 , background : 'var(--color-panel)' , border : '1px solid var(--color-border)' , borderRadius : 6 , padding : '6px 8px' , cursor : 'pointer' , color : 'var(--color-muted)' , lineHeight : 1 } } > ☰</ button >
158140 { mobileOpen && (
159141 < >
160142 < div onClick = { ( ) => setMobileOpen ( false ) } style = { { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,0.6)' , zIndex : 55 , animation : 'fade-in 0.15s ease both' } } />
161- < div style = { { position : 'fixed' , left : 0 , top : 0 , height : '100vh' , zIndex : 60 , animation : 'fade-in 0.2s ease both' } } >
162- { sidebarContent }
163- </ div >
143+ < div style = { { position : 'fixed' , left : 0 , top : 0 , height : '100vh' , zIndex : 60 , animation : 'fade-in 0.2s ease both' } } > { sidebarContent } </ div >
164144 </ >
165145 ) }
166146 </ >
167147 )
168148
169- return (
170- < div style = { { position : 'fixed' , left : 0 , top : 0 , height : '100vh' , zIndex : 50 } } >
171- { sidebarContent }
172- </ div >
173- )
149+ return < div style = { { position : 'fixed' , left : 0 , top : 0 , height : '100vh' , zIndex : 50 } } > { sidebarContent } </ div >
174150}
0 commit comments