homelabby/web/src/pages/DashboardPage.tsx
Mikkel Georgsen 19c2bb7d05 feat(03-03): DashboardPage, ItemDetailPage, FilterBar, and router wiring
- web/src/components/inventory/FilterBar.tsx: search + status dropdown + grid/list toggle
- web/src/pages/DashboardPage.tsx: / route with grid/list view, filters, loading/error/empty states
- web/src/pages/ItemDetailPage.tsx: /item/$id route, two-column desktop, single-column mobile
- web/src/router.tsx: lazy-load DashboardPage + ItemDetailPage, keep intake/scan stubs
2026-04-10 06:21:48 +00:00

90 lines
3.2 KiB
TypeScript

import { useState, useMemo } from 'react'
import { AppShell } from '@/components/layout/AppShell'
import { FilterBar } from '@/components/inventory/FilterBar'
import { ItemCard } from '@/components/inventory/ItemCard'
import { ItemRow } from '@/components/inventory/ItemRow'
import { useInventory } from '@/hooks/useInventory'
import { useUIStore } from '@/store/ui'
import { Loader2, AlertCircle } from 'lucide-react'
export function DashboardPage() {
const { data: items, isLoading, error } = useInventory()
const { viewMode } = useUIStore()
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState('')
const filtered = useMemo(() => {
if (!items) return []
return items.filter((item) => {
const matchSearch =
!search ||
item.name.toLowerCase().includes(search.toLowerCase()) ||
(item.hw_id?.toLowerCase() ?? '').includes(search.toLowerCase()) ||
(item.asset_tag?.toLowerCase() ?? '').includes(search.toLowerCase())
const matchStatus = !statusFilter || item.catalog_status === statusFilter
return matchSearch && matchStatus
})
}, [items, search, statusFilter])
return (
<AppShell>
{/* Page header */}
<div className="mb-6">
<h1 className="font-display font-black text-3xl text-white mb-1">Inventory</h1>
<p className="text-sm text-[#a0a0a0]">All cataloged hardware in your homelab</p>
</div>
<FilterBar
search={search}
onSearchChange={setSearch}
statusFilter={statusFilter}
onStatusChange={setStatusFilter}
totalCount={filtered.length}
/>
{/* Loading */}
{isLoading && (
<div className="flex items-center justify-center py-24 text-[#a0a0a0]">
<Loader2 className="w-6 h-6 animate-spin mr-2" />
Loading inventory
</div>
)}
{/* Error */}
{error && (
<div className="flex items-center gap-3 p-4 border border-red-500/40 rounded-card bg-red-500/10 text-red-400">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">{(error as Error).message}</span>
</div>
)}
{/* Empty state */}
{!isLoading && !error && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-24 text-center">
<p className="font-display font-black text-4xl text-volt mb-2">0</p>
<p className="text-[#a0a0a0] text-sm">
{items && items.length > 0 ? 'No items match your filters' : 'No items cataloged yet — add your first item'}
</p>
</div>
)}
{/* Grid view */}
{!isLoading && !error && filtered.length > 0 && viewMode === 'grid' && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{filtered.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div>
)}
{/* List view */}
{!isLoading && !error && filtered.length > 0 && viewMode === 'list' && (
<div className="border border-charcoal/80 rounded-card overflow-hidden">
{filtered.map((item) => (
<ItemRow key={item.id} item={item} />
))}
</div>
)}
</AppShell>
)
}