234 lines
7.5 KiB
TypeScript
234 lines
7.5 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
|
|
export default function Filters(props: {
|
|
partRoleLabel: string;
|
|
|
|
availableBrands: string[];
|
|
brandFilter: string[];
|
|
setBrandFilter: (v: string[]) => void;
|
|
availableCalibers: string[];
|
|
caliberFilter: string[];
|
|
setCaliberFilter: (next: string[]) => void;
|
|
priceBounds: { min: number | null; max: number | null };
|
|
priceRange: { min: number | null; max: number | null };
|
|
setPriceRange: (v: { min: number | null; max: number | null }) => void;
|
|
|
|
inStockOnly: boolean;
|
|
setInStockOnly: (v: boolean) => void;
|
|
}) {
|
|
const {
|
|
availableBrands,
|
|
brandFilter,
|
|
setBrandFilter,
|
|
availableCalibers,
|
|
caliberFilter,
|
|
setCaliberFilter,
|
|
priceBounds,
|
|
priceRange,
|
|
setPriceRange,
|
|
inStockOnly,
|
|
setInStockOnly,
|
|
} = props;
|
|
|
|
return (
|
|
<aside className="w-full rounded-md border border-zinc-800 bg-zinc-950/80 p-3 space-y-4">
|
|
<div>
|
|
<h2 className="text-xs font-semibold uppercase tracking-[0.16em] text-zinc-500">
|
|
Filters
|
|
</h2>
|
|
<p className="mt-1 text-[11px] text-zinc-500">
|
|
Narrow down the results.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Price range */}
|
|
<div>
|
|
<div className="mb-1.5 flex items-center justify-between">
|
|
<span className="text-[11px] font-medium uppercase tracking-[0.12em] text-zinc-500">
|
|
Price
|
|
</span>
|
|
{(priceRange.min !== null || priceRange.max !== null) && (
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
setPriceRange({
|
|
min: priceBounds.min,
|
|
max: priceBounds.max,
|
|
})
|
|
}
|
|
className="text-[10px] text-zinc-400 hover:text-zinc-200"
|
|
>
|
|
Reset
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{priceBounds.min == null || priceBounds.max == null ? (
|
|
<p className="text-[11px] text-zinc-500">No price data available.</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between text-[10px] text-zinc-400">
|
|
<span>
|
|
Min:{" "}
|
|
<span className="font-semibold text-zinc-200">
|
|
${(priceRange.min ?? priceBounds.min).toFixed(0)}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
Max:{" "}
|
|
<span className="font-semibold text-zinc-200">
|
|
${(priceRange.max ?? priceBounds.max).toFixed(0)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<input
|
|
type="range"
|
|
min={priceBounds.min ?? 0}
|
|
max={priceBounds.max ?? 0}
|
|
value={priceRange.min ?? priceBounds.min ?? 0}
|
|
onChange={(e) => {
|
|
const value = Number(e.target.value);
|
|
setPriceRange({
|
|
min: Math.min(
|
|
value,
|
|
priceRange.max ?? priceBounds.max ?? value
|
|
),
|
|
max: priceRange.max ?? priceBounds.max ?? value,
|
|
});
|
|
}}
|
|
className="w-full"
|
|
/>
|
|
|
|
<input
|
|
type="range"
|
|
min={priceBounds.min ?? 0}
|
|
max={priceBounds.max ?? 0}
|
|
value={priceRange.max ?? priceBounds.max ?? 0}
|
|
onChange={(e) => {
|
|
const value = Number(e.target.value);
|
|
setPriceRange({
|
|
min: priceRange.min ?? priceBounds.min ?? value,
|
|
max: Math.max(
|
|
value,
|
|
priceRange.min ?? priceBounds.min ?? value
|
|
),
|
|
});
|
|
}}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Brand multi-select */}
|
|
<div>
|
|
<div className="mb-1.5 flex items-center justify-between">
|
|
<span className="text-[11px] font-medium uppercase tracking-[0.12em] text-zinc-500">
|
|
Brand
|
|
</span>
|
|
{brandFilter.length > 0 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setBrandFilter([])}
|
|
className="text-[10px] text-zinc-400 hover:text-zinc-200"
|
|
>
|
|
Clear
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{availableBrands.length === 0 ? (
|
|
<p className="text-[11px] text-zinc-500">No brands available yet.</p>
|
|
) : (
|
|
<div className="max-h-56 space-y-1 overflow-y-auto pr-1">
|
|
{availableBrands.map((b) => {
|
|
const checked = brandFilter.includes(b);
|
|
return (
|
|
<label
|
|
key={b}
|
|
className="flex cursor-pointer items-center gap-2 rounded px-1 py-0.5 text-[11px] text-zinc-200 hover:bg-zinc-900/80"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
className="h-3 w-3 rounded border-zinc-600 bg-zinc-900 text-amber-400 focus:ring-amber-400"
|
|
checked={checked}
|
|
onChange={(e) => {
|
|
setBrandFilter(
|
|
e.target.checked
|
|
? [...brandFilter, b]
|
|
: brandFilter.filter((x) => x !== b)
|
|
);
|
|
}}
|
|
/>
|
|
<span className="truncate">{b}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{brandFilter.length === 0 && availableBrands.length > 0 && (
|
|
<p className="mt-1 text-[10px] text-zinc-500">
|
|
No brands selected — showing all.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{availableCalibers?.length > 0 && (
|
|
<div className="mt-4">
|
|
<div className="mb-2 text-xs font-semibold tracking-wide text-zinc-300">
|
|
Caliber
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{availableCalibers.map((c) => {
|
|
const checked = caliberFilter.includes(c);
|
|
return (
|
|
<label
|
|
key={c}
|
|
className="flex items-center gap-2 text-sm text-zinc-300"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={(e) => {
|
|
if (e.target.checked)
|
|
setCaliberFilter([...caliberFilter, c]);
|
|
else
|
|
setCaliberFilter(caliberFilter.filter((x) => x !== c));
|
|
}}
|
|
/>
|
|
<span>{c}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* In-stock toggle */}
|
|
<div className="border-t border-zinc-800 pt-3">
|
|
<label className="flex cursor-pointer items-center justify-between text-[11px] text-zinc-200">
|
|
<span className="font-medium uppercase tracking-[0.12em] text-zinc-500">
|
|
In stock only
|
|
</span>
|
|
<input
|
|
type="checkbox"
|
|
className="h-3 w-3 rounded border-zinc-600 bg-zinc-900 text-amber-400 focus:ring-amber-400"
|
|
checked={inStockOnly}
|
|
onChange={(e) => setInStockOnly(e.target.checked)}
|
|
/>
|
|
</label>
|
|
<p className="mt-1 text-[10px] text-zinc-500">
|
|
Hides items marked out of stock by the backend.
|
|
</p>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|