feat: add budget filtering functionality with min and max budget inputs
This commit is contained in:
parent
c6b325368a
commit
5fee3bc274
9 changed files with 228 additions and 38 deletions
|
|
@ -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 />;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -249,7 +249,8 @@ export type LangDict = {
|
|||
|
||||
comune: string;
|
||||
consegna: string;
|
||||
|
||||
budgetMin: string;
|
||||
budgetMax: string;
|
||||
accordion_minifaq: Faq[];
|
||||
};
|
||||
faq: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,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<string>();
|
||||
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<number>();
|
||||
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 (
|
||||
<RicercaContext.Provider
|
||||
|
|
@ -172,6 +206,12 @@ export const RicercaProvider: FC<{
|
|||
setTipo,
|
||||
tipologieOptions,
|
||||
|
||||
minBudget,
|
||||
setMinBudget,
|
||||
|
||||
maxBudget,
|
||||
setMaxBudget,
|
||||
|
||||
sort,
|
||||
setSort,
|
||||
sortOptions: sortOptions,
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue