feat: refactor Slider component and update budget filters for improved functionality and responsiveness
This commit is contained in:
parent
1e06fd5584
commit
9f02c49193
8 changed files with 134 additions and 140 deletions
|
|
@ -3,26 +3,23 @@ import * as React from "react";
|
|||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface SliderProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
||||
rangeClassName?: string;
|
||||
thumbClassName?: string;
|
||||
}
|
||||
function Slider({
|
||||
className,
|
||||
thumbClassName,
|
||||
rangeClassName,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root> & SliderProps) {
|
||||
const _values = React.useMemo(() => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (Array.isArray(defaultValue)) return defaultValue;
|
||||
return [min, max];
|
||||
}, [value, defaultValue, min, max]);
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
|
|
@ -39,26 +36,22 @@ function Slider({
|
|||
>
|
||||
<SliderPrimitive.Track
|
||||
className={cn(
|
||||
"relative grow cursor-pointer overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5",
|
||||
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
data-slot="slider-track"
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
className={cn(
|
||||
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
rangeClassName,
|
||||
)}
|
||||
data-slot="slider-range"
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
className={cn(
|
||||
"block size-4 shrink-0 cursor-pointer rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50",
|
||||
thumbClassName,
|
||||
)}
|
||||
className="block size-4 shrink-0 cursor-pointer rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50"
|
||||
data-slot="slider-thumb"
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <index is safe here>
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <ok>
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -208,9 +208,7 @@ export const FormNewServizio = ({
|
|||
onValueChange={(vals) => {
|
||||
field.onChange(Number(vals[0]));
|
||||
}}
|
||||
rangeClassName="bg-primary"
|
||||
step={50}
|
||||
thumbClassName="border-primary"
|
||||
value={[form.getValues("budget")]}
|
||||
/>
|
||||
</FormControl>
|
||||
|
|
|
|||
|
|
@ -582,9 +582,7 @@ export const FormNewServizioAcquisto = ({
|
|||
onValueChange={(vals) => {
|
||||
field.onChange(Number(vals[0]));
|
||||
}}
|
||||
rangeClassName="bg-neutral-700"
|
||||
step={50}
|
||||
thumbClassName="border-neutral-700"
|
||||
value={[budget]}
|
||||
/>
|
||||
</FormControl>
|
||||
|
|
|
|||
|
|
@ -283,8 +283,7 @@ export const en: LangDict = {
|
|||
],
|
||||
comune: "City",
|
||||
consegna: "From",
|
||||
budgetMin: "Minimum Budget",
|
||||
budgetMax: "Maximum Budget",
|
||||
budget: "Budget",
|
||||
filtri_attivi: "Active filters",
|
||||
filtri_reset: "Reset filters",
|
||||
filtro_attivo: "Active filter",
|
||||
|
|
|
|||
|
|
@ -288,8 +288,7 @@ export const it: LangDict = {
|
|||
],
|
||||
comune: "Comune",
|
||||
consegna: "Disponibile da",
|
||||
budgetMin: "Budget Minimo",
|
||||
budgetMax: "Budget Massimo",
|
||||
budget: "Budget",
|
||||
filtri_attivi: "Filtri attivi",
|
||||
filtri_reset: "Reset filtri",
|
||||
filtro_attivo: "Filtro attivo",
|
||||
|
|
|
|||
|
|
@ -250,8 +250,7 @@ export type LangDict = {
|
|||
|
||||
comune: string;
|
||||
consegna: string;
|
||||
budgetMin: string;
|
||||
budgetMax: string;
|
||||
budget: string;
|
||||
accordion_minifaq: Faq[];
|
||||
find_similar: string;
|
||||
codice: string;
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ import {
|
|||
import type { GetServerSideProps } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AccordionComp } from "~/components/accordionComp";
|
||||
import { CTA_TipologiaModal } from "~/components/annunci_tutorial";
|
||||
import { LazyCardAnnuncio } from "~/components/annuncio_card";
|
||||
import { CodiceBox } from "~/components/codiceRicerca";
|
||||
import { NumberInput } from "~/components/custom_ui/InputNumber";
|
||||
import { Layout } from "~/components/Layout";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import ScrollToTop from "~/components/scroll_top";
|
||||
|
|
@ -35,11 +34,13 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import { Slider } from "~/components/ui/slider";
|
||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||
import { cn, debounce } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import {
|
||||
type AnnunciOptions,
|
||||
type FilterOption,
|
||||
RicercaProvider,
|
||||
type SortTypes,
|
||||
useRicerca,
|
||||
|
|
@ -72,7 +73,7 @@ const Annunci = ({ options }: AnnunciPageProps) => {
|
|||
<meta content={t.heads.annunci_description} name="description" />
|
||||
</Head>
|
||||
|
||||
<main className="relative mx-auto w-full max-w-10xl space-y-5 bg-background px-2 py-5 md:px-8">
|
||||
<main className="relative mx-auto w-full max-w-500 space-y-5 bg-background px-2 py-5 md:px-8">
|
||||
<Badge className="bg-muted py-1 text-foreground text-sm outline outline-muted-foreground">
|
||||
{t.annunci.titolo}
|
||||
</Badge>
|
||||
|
|
@ -121,6 +122,13 @@ const Ricerca = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const sort_options: FilterOption<SortTypes>[] = [
|
||||
{ value: "Recenti", disabled: false },
|
||||
{ value: "Prezzo", disabled: false },
|
||||
{ value: "Consegna", disabled: false },
|
||||
{ value: "Mq", disabled: false },
|
||||
];
|
||||
|
||||
const Filters = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
|
@ -189,7 +197,7 @@ const Filters = () => {
|
|||
sort: value,
|
||||
});
|
||||
}}
|
||||
options={["Recenti", "Prezzo", "Consegna", "Mq"]}
|
||||
options={sort_options}
|
||||
placeholder={t.seleziona_placeholder}
|
||||
value={sort}
|
||||
/>
|
||||
|
|
@ -210,7 +218,7 @@ const RicercaSideFilters = () => {
|
|||
|
||||
if (!isBigScreen) return null;
|
||||
return (
|
||||
<div className="flex h-fit w-46 max-w-46 shrink-0 grow-0 flex-col gap-3 overflow-clip rounded-md bg-secondary p-2 outline-neutral-300">
|
||||
<div className="flex h-fit w-full max-w-72 shrink-0 grow-0 flex-col gap-3 overflow-clip rounded-md bg-secondary p-2 outline-neutral-300">
|
||||
<Filters />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -240,7 +248,7 @@ const DrowdownFilters = () => {
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="flex w-60 max-w-3xl flex-col gap-2 p-4"
|
||||
className="flex w-full min-w-80 max-w-3xl flex-col gap-2 p-4"
|
||||
>
|
||||
<Filters />
|
||||
</DropdownMenuContent>
|
||||
|
|
@ -253,7 +261,7 @@ const RicercaHeader = () => {
|
|||
const { map, setAllParams } = useRicerca();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-10xl space-y-5">
|
||||
<div className="mx-auto w-full max-w-500 space-y-5">
|
||||
<div className="flex w-full flex-col-reverse gap-2 sm:flex-row">
|
||||
<div className="flex w-full items-center gap-2 sm:w-auto">
|
||||
<DrowdownFilters />
|
||||
|
|
@ -348,7 +356,7 @@ const AnnunciList = () => {
|
|||
interface SelectFilterProps<T> {
|
||||
label: string;
|
||||
id: string;
|
||||
options: string[];
|
||||
options: FilterOption<string>[];
|
||||
defaultValue?: T;
|
||||
value: T | undefined;
|
||||
onValueChange: (value: T) => void;
|
||||
|
|
@ -402,8 +410,13 @@ export const SelectFilter = <T extends string>({
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem className="text-left" key={option} value={option}>
|
||||
{option}
|
||||
<SelectItem
|
||||
className="text-left"
|
||||
disabled={option.disabled}
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
@ -420,7 +433,13 @@ const ConsegnaFilter = () => {
|
|||
if (consegnaOptions.length === 0) return [];
|
||||
return consegnaOptions
|
||||
.map((c) =>
|
||||
t.parametri.mesi[c] ? { value: c, label: t.parametri.mesi[c] } : null,
|
||||
t.parametri.mesi[c.value]
|
||||
? {
|
||||
value: c.value,
|
||||
label: t.parametri.mesi[c.value],
|
||||
disabled: c.disabled,
|
||||
}
|
||||
: null,
|
||||
)
|
||||
.filter((v) => v !== null);
|
||||
}, [consegnaOptions, t.parametri.mesi]);
|
||||
|
|
@ -467,11 +486,12 @@ const ConsegnaFilter = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1 rounded-md bg-background p-1">
|
||||
{mappedConsegnaOptions.map(({ value, label }) => (
|
||||
{mappedConsegnaOptions.map(({ value, label, disabled }) => (
|
||||
<Button
|
||||
className={cn(
|
||||
consegna2?.includes(value) && "border border-primary",
|
||||
)}
|
||||
disabled={disabled && !consegna2?.includes(value)}
|
||||
key={value}
|
||||
onClick={async () => {
|
||||
await onValueChange(value);
|
||||
|
|
@ -479,14 +499,14 @@ const ConsegnaFilter = () => {
|
|||
size="sm"
|
||||
variant={consegna2?.includes(value) ? "default" : "outline"}
|
||||
>
|
||||
<span className="text-sm">{label.substring(0, 3)}</span>
|
||||
<span className="text-sm">{label?.substring(0, 3)}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MAX_SLIDER_VALUE = 1500;
|
||||
const BudgetFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const { minBudget, maxBudget, setAllParams } = useRicerca();
|
||||
|
|
@ -509,80 +529,39 @@ const BudgetFilters = () => {
|
|||
[setAllParams],
|
||||
);
|
||||
|
||||
const [sliderValue, setSliderValue] = useState<[number, number]>([
|
||||
Number(minBudget) / 100 || 0,
|
||||
Number(maxBudget) / 100 || MAX_SLIDER_VALUE,
|
||||
]);
|
||||
|
||||
const onSliderChangeEnd = () => {
|
||||
const [min, max] = sliderValue;
|
||||
minDebounced(min === 0 ? null : min * 100);
|
||||
maxDebounced(max === MAX_SLIDER_VALUE ? null : max * 100);
|
||||
};
|
||||
useEffect(() => {
|
||||
setSliderValue([
|
||||
Number(minBudget) / 100 || 0,
|
||||
Number(maxBudget) / 100 || MAX_SLIDER_VALUE,
|
||||
]);
|
||||
}, [minBudget, maxBudget]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
<div className="flex w-full max-w-md flex-col gap-2">
|
||||
<Label htmlFor="slider">Price Range</Label>
|
||||
<Slider id="slider" max={1000} min={0} onValueChange={setValue} value={value} />
|
||||
<div className="flex items-center justify-between text-muted-foreground text-sm">
|
||||
<span>${value[0]}</span>
|
||||
<span>${value[1]}</span>
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex h-4 flex-row gap-4 px-1">
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<Label
|
||||
className={cn(
|
||||
"font-semibold text-muted-foreground",
|
||||
minBudget !== null && "text-primary",
|
||||
)}
|
||||
htmlFor="prezzoMIN"
|
||||
className={cn("font-semibold text-muted-foreground")}
|
||||
htmlFor="slider"
|
||||
>
|
||||
{t.annunci.budgetMin}
|
||||
{t.annunci.budget}
|
||||
</Label>
|
||||
{minBudget !== null && (
|
||||
{(minBudget !== null || maxBudget !== null) && (
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
await setAllParams({
|
||||
minBudget: null,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<RotateCcwIcon className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<NumberInput
|
||||
className="bg-background"
|
||||
decimalScale={2}
|
||||
decimalSeparator=","
|
||||
fixedDecimalScale
|
||||
id="prezzoMIN"
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
if (v && v > 0) {
|
||||
minDebounced(v * 100);
|
||||
} else {
|
||||
minDebounced(null);
|
||||
}
|
||||
}}
|
||||
stepper={100}
|
||||
suffix=" €"
|
||||
value={Number(minBudget) / 100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex h-4 flex-row gap-4 px-1">
|
||||
<Label
|
||||
className={cn(
|
||||
"font-semibold text-muted-foreground",
|
||||
maxBudget !== null && "text-primary",
|
||||
)}
|
||||
htmlFor="prezzoMAX"
|
||||
>
|
||||
{t.annunci.budgetMax}
|
||||
</Label>
|
||||
{maxBudget !== null && (
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
await setAllParams({
|
||||
maxBudget: null,
|
||||
});
|
||||
}}
|
||||
|
|
@ -592,24 +571,22 @@ const BudgetFilters = () => {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
<NumberInput
|
||||
className="bg-background"
|
||||
decimalScale={2}
|
||||
decimalSeparator=","
|
||||
fixedDecimalScale
|
||||
id="prezzoMAX"
|
||||
<Slider
|
||||
className="mt-2"
|
||||
id="slider"
|
||||
max={MAX_SLIDER_VALUE}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
if (v && v > 0) {
|
||||
maxDebounced(v * 100);
|
||||
} else {
|
||||
maxDebounced(null);
|
||||
}
|
||||
}}
|
||||
stepper={100}
|
||||
suffix=" €"
|
||||
value={Number(maxBudget) / 100}
|
||||
onValueChange={(value) => setSliderValue(value as [number, number])}
|
||||
onValueCommit={onSliderChangeEnd}
|
||||
step={100}
|
||||
value={sliderValue}
|
||||
/>
|
||||
<div className="flex items-center justify-between text-muted-foreground text-sm">
|
||||
<span>{sliderValue[0]} €</span>
|
||||
<span>
|
||||
{sliderValue[1] === MAX_SLIDER_VALUE ? "∞" : sliderValue[1]} €
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ export const buildRicercaUrl = (params: Partial<RicercaUrlParams>): string => {
|
|||
return queryString ? `/annunci?${queryString}` : "/annunci";
|
||||
};
|
||||
|
||||
export type FilterOption<T> = {
|
||||
value: T;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
type RicercaContextType = {
|
||||
map: boolean;
|
||||
tipo: RicercaUrlParams["tipo"];
|
||||
|
|
@ -74,9 +79,9 @@ type RicercaContextType = {
|
|||
) => Promise<URLSearchParams>;
|
||||
reset: () => void;
|
||||
|
||||
comuniOptions: string[];
|
||||
tipologieOptions: string[];
|
||||
consegnaOptions: number[];
|
||||
comuniOptions: FilterOption<string>[];
|
||||
tipologieOptions: FilterOption<string>[];
|
||||
consegnaOptions: FilterOption<number>[];
|
||||
sortOptions: typeof sortOptions;
|
||||
nFiltri: number;
|
||||
isLoading: boolean;
|
||||
|
|
@ -133,11 +138,24 @@ export const RicercaProvider: FC<{
|
|||
}, []);
|
||||
|
||||
const { tipologieOptions, comuniOptions, consegnaOptions } = useMemo(() => {
|
||||
const typeSet = new Set<string>();
|
||||
const comuneSet = new Set<string>();
|
||||
const consegnaSet = new Set<number>();
|
||||
// const typeSet = new Set<string>();
|
||||
// const comuneSet = new Set<string>();
|
||||
// const consegnaSet = new Set<number>();
|
||||
// 1. Sets for ALL distinct values present in the data (The Universe)
|
||||
const allTypes = new Set<string>();
|
||||
const allComuni = new Set<string>();
|
||||
const allConsegna = new Set<number>();
|
||||
|
||||
// 2. Sets for values available with CURRENT OTHER filters (The Subset)
|
||||
const availableTypes = new Set<string>();
|
||||
const availableComuni = new Set<string>();
|
||||
const availableConsegna = new Set<number>();
|
||||
|
||||
for (const item of options) {
|
||||
// Populate the Universe sets
|
||||
allTypes.add(item.tipo);
|
||||
allComuni.add(item.comune);
|
||||
allConsegna.add(item.consegna);
|
||||
const matchesTipo = !tipo || item.tipo === tipo;
|
||||
const matchesComune = !comune || item.comune === comune;
|
||||
const matchesConsegna = !consegna || consegna.includes(item.consegna);
|
||||
|
|
@ -147,30 +165,43 @@ export const RicercaProvider: FC<{
|
|||
|
||||
// Add to Tipologie if it matches everything ELSE (Comune, Consegna, Budget)
|
||||
if (matchesComune && matchesConsegna && matchesBudget) {
|
||||
typeSet.add(item.tipo);
|
||||
availableTypes.add(item.tipo);
|
||||
}
|
||||
|
||||
// Add to Comuni if it matches everything ELSE (Tipo, Consegna, Budget)
|
||||
if (matchesTipo && matchesConsegna && matchesBudget) {
|
||||
comuneSet.add(item.comune);
|
||||
availableComuni.add(item.comune);
|
||||
}
|
||||
|
||||
// Add to Consegna if it matches everything ELSE (Tipo, Comune, Budget)
|
||||
if (matchesTipo && matchesComune && matchesBudget) {
|
||||
consegnaSet.add(item.consegna);
|
||||
availableConsegna.add(item.consegna);
|
||||
}
|
||||
}
|
||||
|
||||
const currentMonth = new Date().getMonth() + 1; // 1-12
|
||||
|
||||
return {
|
||||
tipologieOptions: Array.from(typeSet),
|
||||
comuniOptions: Array.from(comuneSet).sort((a, b) => a.localeCompare(b)),
|
||||
consegnaOptions: Array.from(consegnaSet).sort((a, b) => {
|
||||
tipologieOptions: Array.from(allTypes).map((t) => ({
|
||||
value: t,
|
||||
disabled: !availableTypes.has(t),
|
||||
})),
|
||||
comuniOptions: Array.from(allComuni)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((c) => ({
|
||||
value: c,
|
||||
disabled: !availableComuni.has(c),
|
||||
})),
|
||||
consegnaOptions: Array.from(allConsegna)
|
||||
.sort((a, b) => {
|
||||
const distA = (a - currentMonth + 12) % 12;
|
||||
const distB = (b - currentMonth + 12) % 12;
|
||||
return distA - distB;
|
||||
}),
|
||||
})
|
||||
.map((c) => ({
|
||||
value: c,
|
||||
disabled: !availableConsegna.has(c),
|
||||
})),
|
||||
};
|
||||
}, [options, tipo, comune, consegna, minBudget, maxBudget]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue