Initial commit: Shelfless – alternative Audiobookshelf frontend
React + Vite + TypeScript SPA covering the full ABS feature set (library browsing, item detail, metadata/cover editing, podcasts, player with session sync, admin: users/libraries/scanner/server settings). Dev uses a dynamic CORS proxy; production is served by server/index.mjs (static + reverse proxy to ABS_URL). Includes systemd unit and installer under deploy/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
129
src/components/layout/Sidebar.tsx
Normal file
129
src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import {
|
||||
Home,
|
||||
Settings,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Headphones,
|
||||
BookOpen,
|
||||
Users,
|
||||
FolderCog,
|
||||
ScanLine,
|
||||
ServerCog,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/cn'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { useLibraryStore } from '@/store/libraryStore'
|
||||
import { useIsAdmin } from '@/store/authStore'
|
||||
|
||||
interface ItemProps {
|
||||
to: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
collapsed: boolean
|
||||
end?: boolean
|
||||
}
|
||||
|
||||
function NavItem({ to, label, icon, collapsed, end }: ItemProps) {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
end={end}
|
||||
title={collapsed ? label : undefined}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'group relative flex items-center gap-3 rounded px-3 py-2 text-sm transition-colors',
|
||||
'hover:bg-surface-2',
|
||||
isActive ? 'bg-surface-2 font-medium text-text' : 'text-text-muted',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-r bg-accent transition-opacity',
|
||||
isActive ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span className="shrink-0 text-current">{icon}</span>
|
||||
{!collapsed && <span className="truncate">{label}</span>}
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionLabel({ children, collapsed }: { children: string; collapsed: boolean }) {
|
||||
if (collapsed) return <div className="my-2 border-t border-border" />
|
||||
return (
|
||||
<p className="px-3 pb-1 pt-4 text-[11px] font-semibold uppercase tracking-wider text-text-muted/70">
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const collapsed = useSettingsStore((s) => s.sidebarCollapsed)
|
||||
const toggle = useSettingsStore((s) => s.toggleSidebar)
|
||||
const libraries = useLibraryStore((s) => s.libraries)
|
||||
const isAdmin = useIsAdmin()
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 hidden h-dvh shrink-0 flex-col border-r border-border bg-surface md:flex',
|
||||
collapsed ? 'w-[68px]' : 'w-60',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-14 items-center gap-2 px-4">
|
||||
<div className="grid h-8 w-8 shrink-0 place-items-center rounded bg-accent-soft text-accent">
|
||||
<Headphones size={18} />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="font-heading text-lg font-semibold tracking-tight">Shelfless</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto px-2 py-2">
|
||||
<NavItem to="/" end label="Home" icon={<Home size={18} />} collapsed={collapsed} />
|
||||
|
||||
<SectionLabel collapsed={collapsed}>Bibliotheken</SectionLabel>
|
||||
{libraries.length === 0 && !collapsed && (
|
||||
<p className="px-3 py-1 text-xs text-text-muted/60">Noch keine geladen</p>
|
||||
)}
|
||||
{libraries.map((lib) => (
|
||||
<NavItem
|
||||
key={lib.id}
|
||||
to={`/library/${lib.id}`}
|
||||
label={lib.name}
|
||||
icon={lib.mediaType === 'podcast' ? <Headphones size={18} /> : <BookOpen size={18} />}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<SectionLabel collapsed={collapsed}>Admin</SectionLabel>
|
||||
<NavItem to="/admin/users" label="Benutzer" icon={<Users size={18} />} collapsed={collapsed} />
|
||||
<NavItem to="/admin/libraries" label="Bibliotheken" icon={<FolderCog size={18} />} collapsed={collapsed} />
|
||||
<NavItem to="/admin/scan" label="Scanner" icon={<ScanLine size={18} />} collapsed={collapsed} />
|
||||
<NavItem to="/admin/settings" label="Server" icon={<ServerCog size={18} />} collapsed={collapsed} />
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-border px-2 py-2">
|
||||
<NavItem to="/settings" label="Einstellungen" icon={<Settings size={18} />} collapsed={collapsed} />
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="mt-1 flex w-full items-center gap-3 rounded px-3 py-2 text-sm text-text-muted transition-colors hover:bg-surface-2"
|
||||
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
|
||||
>
|
||||
{collapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
|
||||
{!collapsed && <span>Einklappen</span>}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user