feat: add budget filtering functionality with min and max budget inputs

This commit is contained in:
Marco Pedone 2025-12-29 19:10:46 +01:00
parent c6b325368a
commit 5fee3bc274
9 changed files with 228 additions and 38 deletions

View file

@ -23,7 +23,7 @@ const MapComp = dynamic(() => import("~/components/map/Map"), {
});
export const MapSection = () => {
const { comune, consegna, tipo } = useRicerca();
const { comune, consegna, tipo, minBudget, maxBudget } = useRicerca();
const [selected, setSelected] = useState<AnnuncioRicercaWPosition | null>(
null,
);
@ -33,6 +33,8 @@ export const MapSection = () => {
<MapDisplay
selectComune={comune}
selectConsegna={consegna}
selectMaxBudget={maxBudget}
selectMinBudget={minBudget}
selectTipo={tipo}
setSelected={setSelected}
/>
@ -47,16 +49,22 @@ const MapDisplay = memo(
selectComune,
selectConsegna,
setSelected,
selectMinBudget,
selectMaxBudget,
}: {
selectTipo: string;
selectComune: string;
selectConsegna: number[] | null;
setSelected: Dispatch<SetStateAction<AnnuncioRicercaWPosition | null>>;
selectMinBudget: number | null;
selectMaxBudget: number | null;
}) => {
const { data, isLoading } = api.annunci.getMapping.useQuery({
comune: selectComune,
consegna: selectConsegna || undefined,
tipologia: selectTipo,
minBudget: selectMinBudget || undefined,
maxBudget: selectMaxBudget || undefined,
});
if (isLoading) return <LoadingPage />;

View file

@ -285,6 +285,8 @@ export const en: LangDict = {
],
comune: "City",
consegna: "From",
budgetMin: "Minimum Budget",
budgetMax: "Maximum Budget",
filtri_attivi: "Active filters",
filtri_reset: "Reset filters",
filtro_attivo: "Active filter",

View file

@ -290,6 +290,8 @@ export const it: LangDict = {
],
comune: "Comune",
consegna: "Disponibile da",
budgetMin: "Budget Minimo",
budgetMax: "Budget Massimo",
filtri_attivi: "Filtri attivi",
filtri_reset: "Reset filtri",
filtro_attivo: "Filtro attivo",

View file

@ -249,7 +249,8 @@ export type LangDict = {
comune: string;
consegna: string;
budgetMin: string;
budgetMax: string;
accordion_minifaq: Faq[];
};
faq: {

View file

@ -43,3 +43,18 @@ export function formatCurrency(amount: number): string {
style: "currency",
}).format(amount);
}
export const debounce = <T extends unknown[]>(
callback: (...args: T) => void,
delay: number,
) => {
let timeoutTimer: ReturnType<typeof setTimeout>;
return (...args: T) => {
clearTimeout(timeoutTimer);
timeoutTimer = setTimeout(() => {
callback(...args);
}, delay);
};
};

View file

@ -16,6 +16,7 @@ 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 { Status500 } from "~/components/status-page";
@ -34,7 +35,7 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { cn } from "~/lib/utils";
import { cn, debounce } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import {
type AnnunciOptions,
@ -160,28 +161,7 @@ const Filters = () => {
value={comune}
/>
<ConsegnaFilter />
{/* <SelectFilter
defaultValue={""}
handleReset={async () => {
await setConsegna(null);
}}
label={t.annunci.consegna}
onValueChange={async (value: string) => {
if (value === "") {
await setConsegna(null);
return;
}
if (t.parametri.mesi.includes(value)) {
await setConsegna(t.parametri.mesi.indexOf(value));
return;
}
console.warn("Value not found in parametri.mesi");
await setConsegna(null);
}}
options={mappedConsegnaOptions}
placeholder={t.seleziona_placeholder}
value={consegna ? t.parametri.mesi[consegna] || "" : ""}
/> */}
<BudgetFilters />
<SelectFilter
defaultValue={sort}
handleReset={async () => {
@ -310,6 +290,8 @@ const AnnunciList = () => {
consegna,
sort,
map,
minBudget,
maxBudget,
isLoading: ricercaLoading,
} = useRicerca();
@ -319,6 +301,8 @@ const AnnunciList = () => {
consegna: consegna || undefined,
sort: sort || undefined,
tipologia: tipo || undefined,
minBudget: minBudget || undefined,
maxBudget: maxBudget || undefined,
},
{
enabled: !map,
@ -485,3 +469,111 @@ const ConsegnaFilter = () => {
</div>
);
};
const BudgetFilters = () => {
const { t } = useTranslation();
const { minBudget, setMinBudget, maxBudget, setMaxBudget } = useRicerca();
const minDebounced = useCallback(
debounce(async (value: number | null) => {
await setMinBudget(value);
}, 500),
[setMinBudget],
);
const maxDebounced = useCallback(
debounce(async (value: number | null) => {
await setMaxBudget(value);
}, 500),
[setMaxBudget],
);
return (
<>
<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",
minBudget !== null && "text-primary",
)}
>
{t.annunci.budgetMin}
</Label>
{minBudget !== null && (
<button
className="cursor-pointer"
onClick={async () => {
await setMinBudget(null);
}}
type="button"
>
<RotateCcwIcon className="size-4" />
</button>
)}
</div>
<NumberInput
className="bg-background"
decimalScale={2}
decimalSeparator=","
fixedDecimalScale
id="prezzo"
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",
)}
>
{t.annunci.budgetMax}
</Label>
{maxBudget !== null && (
<button
className="cursor-pointer"
onClick={async () => {
await setMaxBudget(null);
}}
type="button"
>
<RotateCcwIcon className="size-4" />
</button>
)}
</div>
<NumberInput
className="bg-background"
decimalScale={2}
decimalSeparator=","
fixedDecimalScale
id="prezzo"
min={0}
onValueChange={(v) => {
if (v && v > 0) {
maxDebounced(v * 100);
} else {
maxDebounced(null);
}
}}
stepper={100}
suffix=" €"
value={Number(maxBudget) / 100}
/>
</div>
</>
);
};

View file

@ -29,11 +29,20 @@ type RicercaContextType = {
setComune: (
value: string | ((old: string) => string | null) | null,
) => Promise<URLSearchParams>;
consegna: number[] | null;
setConsegna: (
value: number[] | ((old: number[] | null) => number[] | null) | null,
) => Promise<URLSearchParams>;
minBudget: number | null;
setMinBudget: (
value: number | ((old: number | null) => number | null) | null,
) => Promise<URLSearchParams>;
maxBudget: number | null;
setMaxBudget: (
value: number | ((old: number | null) => number | null) | null,
) => Promise<URLSearchParams>;
sort: SortTypes;
setSort: (
value: SortTypes | ((old: SortTypes) => SortTypes | null) | null,
@ -58,6 +67,7 @@ export type AnnunciOptions = {
comune: string;
tipo: string;
consegna: number;
prezzo: number;
}[];
const RicercaContext = createContext<RicercaContextType | undefined>(undefined);
@ -85,13 +95,21 @@ export const RicercaProvider: FC<{
parseAsString.withDefault("").withOptions({ startTransition }),
);
const [comune, setComune] = useQueryState(
"comune",
"com",
parseAsString.withDefault("").withOptions({ startTransition }),
);
const [consegna, setConsegna] = useQueryState(
"consegna",
"cons",
parseAsArrayOf(parseAsInteger).withOptions({ startTransition }),
);
const [minBudget, setMinBudget] = useQueryState(
"minb",
parseAsInteger.withOptions({ startTransition }),
);
const [maxBudget, setMaxBudget] = useQueryState(
"maxb",
parseAsInteger.withOptions({ startTransition }),
);
const [sort, setSort] = useQueryState(
"sort",
parseAsStringLiteral(sortOptions)
@ -104,33 +122,45 @@ export const RicercaProvider: FC<{
options.forEach((item) => {
if (!comune || item.comune === comune) {
if (!consegna || consegna.includes(item.consegna)) {
if (!minBudget || item.prezzo >= minBudget) {
if (!maxBudget || item.prezzo <= maxBudget) {
set.add(item.tipo);
}
}
}
}
});
return Array.from(set);
}, [options, comune, consegna]);
}, [options, comune, consegna, minBudget, maxBudget]);
const comuniOptions = useMemo(() => {
const set = new Set<string>();
options.forEach((item) => {
if (!tipo || item.tipo === tipo) {
if (!consegna || consegna.includes(item.consegna)) {
if (!minBudget || item.prezzo >= minBudget) {
if (!maxBudget || item.prezzo <= maxBudget) {
set.add(item.comune);
}
}
}
}
});
return Array.from(set).sort((a, b) => a.localeCompare(b));
}, [options, tipo, consegna]);
}, [options, tipo, consegna, minBudget, maxBudget]);
const consegnaOptions = useMemo(() => {
const set = new Set<number>();
options.forEach((item) => {
if (!tipo || item.tipo === tipo) {
if (!comune || item.comune === comune) {
if (!minBudget || item.prezzo >= minBudget) {
if (!maxBudget || item.prezzo <= maxBudget) {
set.add(item.consegna);
}
}
}
}
});
const currentMonth = new Date().getMonth() + 1; // 1-12
@ -140,12 +170,14 @@ export const RicercaProvider: FC<{
const distB = (b - currentMonth + 12) % 12;
return distA - distB;
});
}, [options, tipo, comune]);
}, [options, tipo, comune, minBudget, maxBudget]);
const ResetParams = async () => {
await setTipo("");
await setComune("");
await setConsegna(null);
await setMinBudget(null);
await setMaxBudget(null);
await setSort("Recenti");
};
@ -155,7 +187,9 @@ export const RicercaProvider: FC<{
0,
) +
(consegna !== null ? 1 : 0) +
(sort !== "Recenti" ? 1 : 0);
(sort !== "Recenti" ? 1 : 0) +
(minBudget !== null ? 1 : 0) +
(maxBudget !== null ? 1 : 0);
return (
<RicercaContext.Provider
@ -172,6 +206,12 @@ export const RicercaProvider: FC<{
setTipo,
tipologieOptions,
minBudget,
setMinBudget,
maxBudget,
setMaxBudget,
sort,
setSort,
sortOptions: sortOptions,

View file

@ -73,6 +73,8 @@ export const annunciRouter = createTRPCRouter({
comune: z.string().optional(),
consegna: z.number().array().optional(),
tipologia: z.string().optional(),
minBudget: z.number().optional(),
maxBudget: z.number().optional(),
}),
)
.query(async ({ input }) => {
@ -97,6 +99,8 @@ export const annunciRouter = createTRPCRouter({
consegna: z.number().array().optional(),
sort: z.enum(["Recenti", "Prezzo", "Consegna", "Mq"]).optional(),
tipologia: z.string().optional(),
minBudget: z.number().optional(),
maxBudget: z.number().optional(),
}),
)
.query(async ({ input }) => {

View file

@ -177,10 +177,14 @@ export const get_AnnunciPositionsHandler = async ({
tipologia,
comune,
consegna,
minBudget,
maxBudget,
}: {
tipologia?: string;
comune?: string;
consegna?: number[];
minBudget?: number;
maxBudget?: number;
}): Promise<AnnuncioRicercaWPosition[]> => {
try {
let query = db
@ -224,6 +228,12 @@ export const get_AnnunciPositionsHandler = async ({
if (consegna) {
query = query.where("consegna", "in", consegna);
}
if (minBudget) {
query = query.where("prezzo", ">=", minBudget);
}
if (maxBudget) {
query = query.where("prezzo", "<=", maxBudget);
}
const annunci = await query.execute();
if (!annunci) {
@ -270,11 +280,15 @@ export const getAnnunciRicerca = async ({
comune,
consegna,
sort,
minBudget,
maxBudget,
}: {
tipologia?: string;
comune?: string;
consegna?: number[];
sort?: "Recenti" | "Prezzo" | "Consegna" | "Mq";
minBudget?: number;
maxBudget?: number;
}): Promise<AnnuncioRicerca[]> => {
try {
let query = db
@ -315,6 +329,12 @@ export const getAnnunciRicerca = async ({
if (consegna) {
query = query.where("consegna", "in", consegna);
}
if (minBudget) {
query = query.where("prezzo", ">=", minBudget);
}
if (maxBudget) {
query = query.where("prezzo", "<=", maxBudget);
}
switch (sort) {
case "Recenti":
@ -348,19 +368,25 @@ export const getOptions_AnnunciHandler = async () => {
try {
const comuni = await db
.selectFrom("annunci")
.select(["comune", "tipo", "consegna"])
.select(["comune", "tipo", "consegna", "prezzo"])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")
.distinct()
.execute();
const results: { comune: string; tipo: string; consegna: number }[] = [];
const results: {
comune: string;
tipo: string;
consegna: number;
prezzo: number;
}[] = [];
for (const comune of comuni) {
if (comune.comune && comune.tipo && comune.consegna) {
if (comune.comune && comune.tipo && comune.consegna && comune.prezzo) {
results.push({
comune: comune.comune,
consegna: comune.consegna,
tipo: comune.tipo,
prezzo: comune.prezzo,
});
}
}