Skip to content

Commit c2be23a

Browse files
committed
merge: sidebar migliorata — collapse/expand animato, favorites, badge notifiche, footer versione
2 parents eb4592c + 5e643c5 commit c2be23a

2 files changed

Lines changed: 106 additions & 130 deletions

File tree

web/app/components/main-content.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function MainContent({ children }: { children: React.ReactNode })
2222
}, [])
2323

2424
const hasSidebar = pathname !== '/' && !PROTECTED_PREFIXES.some(p => pathname === p || pathname.startsWith(p + '/'))
25-
const marginLeft = hasSidebar && !isMobile ? 200 : 0
25+
const marginLeft = hasSidebar && !isMobile ? 'var(--sidebar-w, 200px)' : 0
2626

2727
return (
2828
<div id="main-content" tabIndex={-1} style={{ marginLeft, minHeight: '100vh', position: 'relative', zIndex: 1 }}>

web/app/components/sidebar.tsx

Lines changed: 105 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -6,169 +6,145 @@ import { useState, useEffect } from 'react'
66
import LanguageSwitcher from './LanguageSwitcher'
77
import { 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+
8722
export 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

Comments
 (0)