Redesign filter panel with range sliders, separated visualization card, and backend filter support
Simplify the filter UI to show only essential filters (type toggle, price/bedroom range sliders, min size) by default, with advanced filters collapsed. Extract visualization controls (color-by metric, POI travel mode) into a separate VisualizationCard component. Wire up previously ignored backend filters: max_sqm, min/max_price_per_sqm, and district_names now work end-to-end.
This commit is contained in:
parent
1f4a3f858c
commit
743e018668
11 changed files with 422 additions and 588 deletions
70
frontend/src/components/ui/range-slider-field.tsx
Normal file
70
frontend/src/components/ui/range-slider-field.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { Slider } from "./slider";
|
||||
import { Input } from "./input";
|
||||
|
||||
interface RangeSliderFieldProps {
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
value: [number, number];
|
||||
onValueChange: (value: [number, number]) => void;
|
||||
formatValue?: (v: number) => string;
|
||||
}
|
||||
|
||||
export function RangeSliderField({
|
||||
label,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
value,
|
||||
onValueChange,
|
||||
formatValue = (v) => String(v),
|
||||
}: RangeSliderFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">{label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatValue(value[0])} – {formatValue(value[1])}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onValueChange={(v) => onValueChange(v as [number, number])}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-7 text-xs"
|
||||
min={min}
|
||||
max={value[1]}
|
||||
step={step}
|
||||
value={value[0]}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
if (!isNaN(v)) {
|
||||
onValueChange([Math.min(v, value[1]), value[1]]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
className="h-7 text-xs"
|
||||
min={value[0]}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value[1]}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
if (!isNaN(v)) {
|
||||
onValueChange([value[0], Math.max(v, value[0])]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ const Slider = React.forwardRef<
|
|||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
{props.defaultValue?.map((_, index) => (
|
||||
{(props.defaultValue ?? props.value)?.map((_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
key={index}
|
||||
className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue