Add category grouping to sidebar and dashboard

Sidebar now renders uncategorised apps flat and categorised apps in
collapsible accordion sections; active category stays expanded. Dashboard
groups tiles under category section headings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-01 18:26:09 +00:00
parent 15c2064483
commit affb1368ac
3 changed files with 153 additions and 33 deletions

View file

@ -7,6 +7,16 @@ import type { User, App } from '../types'
export function Dashboard({ user }: { user: User }) {
const navigate = useNavigate()
const uncategorised = user.apps.filter(a => !a.category)
const grouped = user.apps.reduce<Record<string, App[]>>((acc, app) => {
if (!app.category) return acc
if (!acc[app.category]) acc[app.category] = []
acc[app.category].push(app)
return acc
}, {})
const categoryNames = Object.keys(grouped).sort()
const hasAny = user.apps.length > 0
return (
<div style={{ display: 'flex', height: '100dvh' }}>
<Sidebar user={user} />
@ -16,25 +26,48 @@ export function Dashboard({ user }: { user: User }) {
<span style={{ color: 'var(--text-dark)', fontWeight: 600 }}>{user.name}</span>
</p>
{user.apps.length === 0 ? (
{!hasAny && (
<p style={{ color: 'var(--text-mid)', padding: '3rem 0', textAlign: 'center' }}>
No apps assigned yet. Contact an administrator.
</p>
) : (
<div style={{
display: 'grid', gap: '1rem',
gridTemplateColumns: 'repeat(auto-fill, minmax(210px, 1fr))',
}}>
{user.apps.map(app => (
<AppTile key={app.slug} app={app} onOpen={() => navigate(`/app/${app.slug}`)} />
))}
</div>
)}
{/* Uncategorised apps */}
{uncategorised.length > 0 && (
<AppGrid apps={uncategorised} onOpen={slug => navigate(`/app/${slug}`)} />
)}
{/* Categorised groups */}
{categoryNames.map(cat => (
<div key={cat} style={{ marginTop: uncategorised.length > 0 ? '2rem' : 0 }}>
<h2 style={{
fontSize: '0.72rem', fontWeight: 700, color: 'var(--text-mid)',
textTransform: 'uppercase', letterSpacing: '0.1em',
marginBottom: '0.875rem',
}}>
{cat}
</h2>
<AppGrid apps={grouped[cat]} onOpen={slug => navigate(`/app/${slug}`)} />
</div>
))}
</main>
</div>
)
}
function AppGrid({ apps, onOpen }: { apps: App[]; onOpen: (slug: string) => void }) {
return (
<div style={{
display: 'grid', gap: '1rem',
gridTemplateColumns: 'repeat(auto-fill, minmax(210px, 1fr))',
}}>
{apps.map(app => (
<AppTile key={app.slug} app={app} onOpen={() => onOpen(app.slug)} />
))}
</div>
)
}
function AppTile({ app, onOpen }: { app: App; onOpen: () => void }) {
return (
<div style={{