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

View file

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

View file

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

View file

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

View file

@ -43,3 +43,18 @@ export function formatCurrency(amount: number): string {
style: "currency", style: "currency",
}).format(amount); }).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 { CTA_TipologiaModal } from "~/components/annunci_tutorial";
import { LazyCardAnnuncio } from "~/components/annuncio_card"; import { LazyCardAnnuncio } from "~/components/annuncio_card";
import { CodiceBox } from "~/components/codiceRicerca"; import { CodiceBox } from "~/components/codiceRicerca";
import { NumberInput } from "~/components/custom_ui/InputNumber";
import { Layout } from "~/components/Layout"; import { Layout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
@ -34,7 +35,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "~/components/ui/select"; } from "~/components/ui/select";
import { cn } from "~/lib/utils"; import { cn, debounce } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { import {
type AnnunciOptions, type AnnunciOptions,
@ -160,28 +161,7 @@ const Filters = () => {
value={comune} value={comune}
/> />
<ConsegnaFilter /> <ConsegnaFilter />
{/* <SelectFilter <BudgetFilters />
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] || "" : ""}
/> */}
<SelectFilter <SelectFilter
defaultValue={sort} defaultValue={sort}
handleReset={async () => { handleReset={async () => {
@ -310,6 +290,8 @@ const AnnunciList = () => {
consegna, consegna,
sort, sort,
map, map,
minBudget,
maxBudget,
isLoading: ricercaLoading, isLoading: ricercaLoading,
} = useRicerca(); } = useRicerca();
@ -319,6 +301,8 @@ const AnnunciList = () => {
consegna: consegna || undefined, consegna: consegna || undefined,
sort: sort || undefined, sort: sort || undefined,
tipologia: tipo || undefined, tipologia: tipo || undefined,
minBudget: minBudget || undefined,
maxBudget: maxBudget || undefined,
}, },
{ {
enabled: !map, enabled: !map,
@ -485,3 +469,111 @@ const ConsegnaFilter = () => {
</div> </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: ( setComune: (
value: string | ((old: string) => string | null) | null, value: string | ((old: string) => string | null) | null,
) => Promise<URLSearchParams>; ) => Promise<URLSearchParams>;
consegna: number[] | null; consegna: number[] | null;
setConsegna: ( setConsegna: (
value: number[] | ((old: number[] | null) => number[] | null) | null, value: number[] | ((old: number[] | null) => number[] | null) | null,
) => Promise<URLSearchParams>; ) => 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; sort: SortTypes;
setSort: ( setSort: (
value: SortTypes | ((old: SortTypes) => SortTypes | null) | null, value: SortTypes | ((old: SortTypes) => SortTypes | null) | null,
@ -58,6 +67,7 @@ export type AnnunciOptions = {
comune: string; comune: string;
tipo: string; tipo: string;
consegna: number; consegna: number;
prezzo: number;
}[]; }[];
const RicercaContext = createContext<RicercaContextType | undefined>(undefined); const RicercaContext = createContext<RicercaContextType | undefined>(undefined);
@ -85,13 +95,21 @@ export const RicercaProvider: FC<{
parseAsString.withDefault("").withOptions({ startTransition }), parseAsString.withDefault("").withOptions({ startTransition }),
); );
const [comune, setComune] = useQueryState( const [comune, setComune] = useQueryState(
"comune", "com",
parseAsString.withDefault("").withOptions({ startTransition }), parseAsString.withDefault("").withOptions({ startTransition }),
); );
const [consegna, setConsegna] = useQueryState( const [consegna, setConsegna] = useQueryState(
"consegna", "cons",
parseAsArrayOf(parseAsInteger).withOptions({ startTransition }), parseAsArrayOf(parseAsInteger).withOptions({ startTransition }),
); );
const [minBudget, setMinBudget] = useQueryState(
"minb",
parseAsInteger.withOptions({ startTransition }),
);
const [maxBudget, setMaxBudget] = useQueryState(
"maxb",
parseAsInteger.withOptions({ startTransition }),
);
const [sort, setSort] = useQueryState( const [sort, setSort] = useQueryState(
"sort", "sort",
parseAsStringLiteral(sortOptions) parseAsStringLiteral(sortOptions)
@ -104,33 +122,45 @@ export const RicercaProvider: FC<{
options.forEach((item) => { options.forEach((item) => {
if (!comune || item.comune === comune) { if (!comune || item.comune === comune) {
if (!consegna || consegna.includes(item.consegna)) { if (!consegna || consegna.includes(item.consegna)) {
if (!minBudget || item.prezzo >= minBudget) {
if (!maxBudget || item.prezzo <= maxBudget) {
set.add(item.tipo); set.add(item.tipo);
} }
} }
}
}
}); });
return Array.from(set); return Array.from(set);
}, [options, comune, consegna]); }, [options, comune, consegna, minBudget, maxBudget]);
const comuniOptions = useMemo(() => { const comuniOptions = useMemo(() => {
const set = new Set<string>(); const set = new Set<string>();
options.forEach((item) => { options.forEach((item) => {
if (!tipo || item.tipo === tipo) { if (!tipo || item.tipo === tipo) {
if (!consegna || consegna.includes(item.consegna)) { if (!consegna || consegna.includes(item.consegna)) {
if (!minBudget || item.prezzo >= minBudget) {
if (!maxBudget || item.prezzo <= maxBudget) {
set.add(item.comune); set.add(item.comune);
} }
} }
}
}
}); });
return Array.from(set).sort((a, b) => a.localeCompare(b)); return Array.from(set).sort((a, b) => a.localeCompare(b));
}, [options, tipo, consegna]); }, [options, tipo, consegna, minBudget, maxBudget]);
const consegnaOptions = useMemo(() => { const consegnaOptions = useMemo(() => {
const set = new Set<number>(); const set = new Set<number>();
options.forEach((item) => { options.forEach((item) => {
if (!tipo || item.tipo === tipo) { if (!tipo || item.tipo === tipo) {
if (!comune || item.comune === comune) { if (!comune || item.comune === comune) {
if (!minBudget || item.prezzo >= minBudget) {
if (!maxBudget || item.prezzo <= maxBudget) {
set.add(item.consegna); set.add(item.consegna);
} }
} }
}
}
}); });
const currentMonth = new Date().getMonth() + 1; // 1-12 const currentMonth = new Date().getMonth() + 1; // 1-12
@ -140,12 +170,14 @@ export const RicercaProvider: FC<{
const distB = (b - currentMonth + 12) % 12; const distB = (b - currentMonth + 12) % 12;
return distA - distB; return distA - distB;
}); });
}, [options, tipo, comune]); }, [options, tipo, comune, minBudget, maxBudget]);
const ResetParams = async () => { const ResetParams = async () => {
await setTipo(""); await setTipo("");
await setComune(""); await setComune("");
await setConsegna(null); await setConsegna(null);
await setMinBudget(null);
await setMaxBudget(null);
await setSort("Recenti"); await setSort("Recenti");
}; };
@ -155,7 +187,9 @@ export const RicercaProvider: FC<{
0, 0,
) + ) +
(consegna !== null ? 1 : 0) + (consegna !== null ? 1 : 0) +
(sort !== "Recenti" ? 1 : 0); (sort !== "Recenti" ? 1 : 0) +
(minBudget !== null ? 1 : 0) +
(maxBudget !== null ? 1 : 0);
return ( return (
<RicercaContext.Provider <RicercaContext.Provider
@ -172,6 +206,12 @@ export const RicercaProvider: FC<{
setTipo, setTipo,
tipologieOptions, tipologieOptions,
minBudget,
setMinBudget,
maxBudget,
setMaxBudget,
sort, sort,
setSort, setSort,
sortOptions: sortOptions, sortOptions: sortOptions,

View file

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

View file

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