From 5fee3bc274eb74da3a974db80969e7c15fe468da Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Mon, 29 Dec 2025 19:10:46 +0100 Subject: [PATCH] feat: add budget filtering functionality with min and max budget inputs --- .../src/components/annunci_map.tsx | 10 +- apps/infoalloggi/src/i18n/en.ts | 2 + apps/infoalloggi/src/i18n/it.ts | 2 + apps/infoalloggi/src/i18n/locales.ts | 3 +- apps/infoalloggi/src/lib/utils.ts | 15 ++ apps/infoalloggi/src/pages/annunci.tsx | 138 +++++++++++++++--- .../src/providers/RicercaProvider.tsx | 60 ++++++-- .../src/server/api/routers/annunci.ts | 4 + .../server/controllers/annunci.controller.ts | 32 +++- 9 files changed, 228 insertions(+), 38 deletions(-) diff --git a/apps/infoalloggi/src/components/annunci_map.tsx b/apps/infoalloggi/src/components/annunci_map.tsx index befb373..ea0e4fc 100644 --- a/apps/infoalloggi/src/components/annunci_map.tsx +++ b/apps/infoalloggi/src/components/annunci_map.tsx @@ -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( null, ); @@ -33,6 +33,8 @@ export const MapSection = () => { @@ -47,16 +49,22 @@ const MapDisplay = memo( selectComune, selectConsegna, setSelected, + selectMinBudget, + selectMaxBudget, }: { selectTipo: string; selectComune: string; selectConsegna: number[] | null; setSelected: Dispatch>; + 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 ; diff --git a/apps/infoalloggi/src/i18n/en.ts b/apps/infoalloggi/src/i18n/en.ts index 04c8987..709aeae 100644 --- a/apps/infoalloggi/src/i18n/en.ts +++ b/apps/infoalloggi/src/i18n/en.ts @@ -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", diff --git a/apps/infoalloggi/src/i18n/it.ts b/apps/infoalloggi/src/i18n/it.ts index d17a8f8..967c86b 100644 --- a/apps/infoalloggi/src/i18n/it.ts +++ b/apps/infoalloggi/src/i18n/it.ts @@ -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", diff --git a/apps/infoalloggi/src/i18n/locales.ts b/apps/infoalloggi/src/i18n/locales.ts index 2857313..5588d77 100644 --- a/apps/infoalloggi/src/i18n/locales.ts +++ b/apps/infoalloggi/src/i18n/locales.ts @@ -249,7 +249,8 @@ export type LangDict = { comune: string; consegna: string; - + budgetMin: string; + budgetMax: string; accordion_minifaq: Faq[]; }; faq: { diff --git a/apps/infoalloggi/src/lib/utils.ts b/apps/infoalloggi/src/lib/utils.ts index 411ba59..0a4e5ea 100644 --- a/apps/infoalloggi/src/lib/utils.ts +++ b/apps/infoalloggi/src/lib/utils.ts @@ -43,3 +43,18 @@ export function formatCurrency(amount: number): string { style: "currency", }).format(amount); } + +export const debounce = ( + callback: (...args: T) => void, + delay: number, +) => { + let timeoutTimer: ReturnType; + + return (...args: T) => { + clearTimeout(timeoutTimer); + + timeoutTimer = setTimeout(() => { + callback(...args); + }, delay); + }; +}; diff --git a/apps/infoalloggi/src/pages/annunci.tsx b/apps/infoalloggi/src/pages/annunci.tsx index fc6f30c..ce44d5a 100644 --- a/apps/infoalloggi/src/pages/annunci.tsx +++ b/apps/infoalloggi/src/pages/annunci.tsx @@ -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} /> - {/* { - 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] || "" : ""} - /> */} + { @@ -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 = () => { ); }; + +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 ( + <> +
+
+ + {minBudget !== null && ( + + )} +
+ + { + if (v && v > 0) { + minDebounced(v * 100); + } else { + minDebounced(null); + } + }} + stepper={100} + suffix=" €" + value={Number(minBudget) / 100} + /> +
+ +
+
+ + {maxBudget !== null && ( + + )} +
+ { + if (v && v > 0) { + maxDebounced(v * 100); + } else { + maxDebounced(null); + } + }} + stepper={100} + suffix=" €" + value={Number(maxBudget) / 100} + /> +
+ + ); +}; diff --git a/apps/infoalloggi/src/providers/RicercaProvider.tsx b/apps/infoalloggi/src/providers/RicercaProvider.tsx index 38eca2e..72bfae6 100644 --- a/apps/infoalloggi/src/providers/RicercaProvider.tsx +++ b/apps/infoalloggi/src/providers/RicercaProvider.tsx @@ -29,11 +29,20 @@ type RicercaContextType = { setComune: ( value: string | ((old: string) => string | null) | null, ) => Promise; - consegna: number[] | null; setConsegna: ( value: number[] | ((old: number[] | null) => number[] | null) | null, ) => Promise; + + minBudget: number | null; + setMinBudget: ( + value: number | ((old: number | null) => number | null) | null, + ) => Promise; + maxBudget: number | null; + setMaxBudget: ( + value: number | ((old: number | null) => number | null) | null, + ) => Promise; + 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(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,31 +122,43 @@ export const RicercaProvider: FC<{ options.forEach((item) => { if (!comune || item.comune === comune) { if (!consegna || consegna.includes(item.consegna)) { - set.add(item.tipo); + 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(); options.forEach((item) => { if (!tipo || item.tipo === tipo) { if (!consegna || consegna.includes(item.consegna)) { - set.add(item.comune); + 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(); options.forEach((item) => { if (!tipo || item.tipo === tipo) { if (!comune || item.comune === comune) { - set.add(item.consegna); + if (!minBudget || item.prezzo >= minBudget) { + if (!maxBudget || item.prezzo <= maxBudget) { + set.add(item.consegna); + } + } } } }); @@ -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 ( { @@ -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 }) => { diff --git a/apps/infoalloggi/src/server/controllers/annunci.controller.ts b/apps/infoalloggi/src/server/controllers/annunci.controller.ts index 720f697..3b0031b 100644 --- a/apps/infoalloggi/src/server/controllers/annunci.controller.ts +++ b/apps/infoalloggi/src/server/controllers/annunci.controller.ts @@ -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 => { 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 => { 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, }); } }