Files
pca-pijac/frontend/src/components/vendors/vendor-toolbar.tsx
Marko Marković b481531301 Extract vendor page subcomponents; structured Directus errors
Split the 316-line vendors index into VendorToolbar, VendorGrid,
VendorList, and PerPageSelector under components/vendors/. Page now
owns only search-param state and query orchestration.

Replace generic Error throws in lib/directus.ts with a DirectusError
class carrying status, statusText, collection, url, and parsed body.
Surface useLocalStorage read/write failures via console.warn instead
of swallowing them.
2026-04-23 21:52:15 +04:00

114 lines
3.6 KiB
TypeScript

import { Check, Filter, FilterX, LayoutGrid, List, Loader2, Search, X } from 'lucide-react'
import { Button } from '~/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '~/components/ui/dropdown-menu'
import { Input } from '~/components/ui/input'
import type { Category } from '~/lib/types'
import type { VendorLayout } from './types'
type Props = {
searchInput: string
onSearchInput: (value: string) => void
onSearchClear: () => void
isFetching: boolean
categories: Category[]
category: string
onCategoryChange: (slug: string | undefined) => void
layout: VendorLayout
onLayoutChange: (layout: VendorLayout) => void
}
export function VendorToolbar({
searchInput,
onSearchInput,
onSearchClear,
isFetching,
categories,
category,
onCategoryChange,
layout,
onLayoutChange,
}: Props) {
return (
<div className="mb-5 flex flex-col items-stretch gap-4 md:flex-row md:items-center md:justify-between">
<h1 className="text-2xl font-bold">Vendors</h1>
<div className="flex h-9 items-center justify-center">
{isFetching && <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />}
</div>
<div className="flex flex-wrap items-center gap-3">
<div className="relative">
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-9 pr-9"
placeholder="Search vendors"
value={searchInput}
onChange={(e) => onSearchInput(e.target.value.toLowerCase())}
/>
{searchInput && (
<button
type="button"
aria-label="Clear search"
onClick={onSearchClear}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" aria-label="Filter by category">
{category ? <FilterX /> : <Filter />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{categories.map((cat) => (
<DropdownMenuItem
key={cat.slug}
disabled={category === cat.slug}
onSelect={() => onCategoryChange(cat.slug)}
>
{category === cat.slug && <Check className="h-4 w-4" />}
{cat.name}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => onCategoryChange(undefined)}>Clear</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="inline-flex rounded-md border">
<Button
variant={layout === 'GRID' ? 'secondary' : 'ghost'}
size="icon"
className="rounded-r-none"
onClick={() => onLayoutChange('GRID')}
aria-label="Grid layout"
disabled={layout === 'GRID'}
>
<LayoutGrid />
</Button>
<Button
variant={layout === 'LIST' ? 'secondary' : 'ghost'}
size="icon"
className="rounded-l-none border-l"
onClick={() => onLayoutChange('LIST')}
aria-label="List layout"
disabled={layout === 'LIST'}
>
<List />
</Button>
</div>
</div>
</div>
)
}