2025-08-28 18:27:07 +02:00
|
|
|
import { keepPreviousData } from "@tanstack/react-query";
|
|
|
|
|
import {
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronUp,
|
|
|
|
|
Filter,
|
|
|
|
|
FilterX,
|
|
|
|
|
MapIcon,
|
|
|
|
|
RotateCcwIcon,
|
|
|
|
|
Table,
|
|
|
|
|
} from "lucide-react";
|
2025-08-04 17:45:44 +02:00
|
|
|
import type { GetServerSideProps } from "next";
|
2025-11-13 11:38:23 +01:00
|
|
|
import dynamic from "next/dynamic";
|
2025-08-04 17:45:44 +02:00
|
|
|
import Head from "next/head";
|
2025-11-11 17:15:29 +01:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { AccordionComp } from "~/components/accordionComp";
|
|
|
|
|
import { CTA_TipologiaModal } from "~/components/annunci_tutorial";
|
2025-11-12 15:52:04 +01:00
|
|
|
import { LazyCardAnnuncio } from "~/components/annuncio_card";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { CodiceBox } from "~/components/codiceRicerca";
|
2025-11-03 18:49:27 +01:00
|
|
|
import { Layout } from "~/components/Layout";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { LoadingPage } from "~/components/loading";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { Status500 } from "~/components/status-page";
|
|
|
|
|
import { Badge } from "~/components/ui/badge";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { Button } from "~/components/ui/button";
|
2025-08-28 18:27:07 +02:00
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "~/components/ui/dropdown-menu";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { Label } from "~/components/ui/label";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "~/components/ui/select";
|
|
|
|
|
import { cn } from "~/lib/utils";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
type AnnunciOptions,
|
|
|
|
|
RicercaProvider,
|
|
|
|
|
type SortTypes,
|
|
|
|
|
useRicerca,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/providers/RicercaProvider";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
|
|
|
|
import { api } from "~/utils/api";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-11-13 11:38:23 +01:00
|
|
|
const MapSection = dynamic(
|
|
|
|
|
() =>
|
|
|
|
|
import("~/components/annunci_map").then((mod) => ({
|
|
|
|
|
default: mod.MapSection,
|
|
|
|
|
})),
|
|
|
|
|
{
|
|
|
|
|
ssr: false,
|
|
|
|
|
loading: () => <LoadingPage />,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2025-08-04 17:45:44 +02:00
|
|
|
type AnnunciPageProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
options: AnnunciOptions;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const Annunci = ({ options }: AnnunciPageProps) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { t } = useTranslation();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<RicercaProvider options={options}>
|
|
|
|
|
<Head>
|
|
|
|
|
<title>{t.heads.annunci_titolo}</title>
|
2025-08-29 16:18:32 +02:00
|
|
|
<meta content={t.heads.annunci_description} name="description" />
|
2025-08-28 18:27:07 +02:00
|
|
|
</Head>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-11-12 15:52:04 +01:00
|
|
|
<main className="relative mx-auto w-full max-w-9xl space-y-5 bg-background2 px-2 py-5 md:px-8">
|
2025-11-03 18:49:27 +01:00
|
|
|
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
|
2025-09-01 17:31:19 +02:00
|
|
|
{t.annunci.titolo}
|
|
|
|
|
</div>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-11-03 18:49:27 +01:00
|
|
|
<GoBackButton />
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
<Ricerca />
|
|
|
|
|
<AccordionComp
|
|
|
|
|
className="max-w-6xl px-4 py-4 md:py-8"
|
2025-11-03 18:49:27 +01:00
|
|
|
defaultOpen={0}
|
2025-08-29 16:18:32 +02:00
|
|
|
texts={t.annunci.accordion_minifaq}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
</main>
|
|
|
|
|
</RicercaProvider>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
2025-11-03 18:49:27 +01:00
|
|
|
const GoBackButton = () => {
|
|
|
|
|
const { page, setPage } = useRicerca();
|
|
|
|
|
if (page >= 2) {
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
className="absolute right-0 h-auto py-0 text-base text-neutral-600 underline underline-offset-1 sm:hidden"
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
await setPage(0);
|
|
|
|
|
}}
|
|
|
|
|
variant="link"
|
|
|
|
|
>
|
|
|
|
|
Torna all'inizio
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-08-04 17:45:44 +02:00
|
|
|
export default Annunci;
|
|
|
|
|
|
2025-11-03 18:49:27 +01:00
|
|
|
Annunci.getLayout = (page: React.ReactNode) => {
|
|
|
|
|
return (
|
|
|
|
|
<Layout
|
|
|
|
|
containerClassName="bg-background2"
|
|
|
|
|
footerClassName="bg-background2"
|
|
|
|
|
>
|
|
|
|
|
{page}
|
|
|
|
|
</Layout>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-04 17:45:44 +02:00
|
|
|
export const getServerSideProps = (async () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const helper = generateSSGHelper();
|
|
|
|
|
const options = await helper.annunci.getAnnunciOptions.fetch();
|
|
|
|
|
return {
|
|
|
|
|
props: {
|
|
|
|
|
options,
|
|
|
|
|
},
|
|
|
|
|
};
|
2025-08-04 17:45:44 +02:00
|
|
|
}) satisfies GetServerSideProps<AnnunciPageProps>;
|
|
|
|
|
|
|
|
|
|
const Ricerca = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { map, page } = useRicerca();
|
|
|
|
|
useEffect(() => {
|
2025-08-29 16:18:32 +02:00
|
|
|
window.scrollTo({ behavior: "smooth", top: 0 });
|
2025-08-28 18:27:07 +02:00
|
|
|
}, [page]);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2025-11-12 15:52:04 +01:00
|
|
|
<div className="mx-auto w-full space-y-5">
|
2025-11-11 17:15:29 +01:00
|
|
|
<RicercaHeader />
|
2025-11-12 15:52:04 +01:00
|
|
|
<div className="relative flex w-full gap-4">
|
2025-11-11 17:15:29 +01:00
|
|
|
<RicercaSideFilters />
|
|
|
|
|
<div className="flex-1">{map ? <MapSection /> : <AnnunciList />}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
2025-11-03 18:49:27 +01:00
|
|
|
|
2025-11-12 15:52:04 +01:00
|
|
|
const Filters = () => {
|
2025-11-11 17:15:29 +01:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const {
|
|
|
|
|
tipo,
|
|
|
|
|
setTipo,
|
|
|
|
|
comune,
|
|
|
|
|
setComune,
|
|
|
|
|
consegna,
|
|
|
|
|
setConsegna,
|
|
|
|
|
sort,
|
|
|
|
|
setSort,
|
|
|
|
|
comuniOptions,
|
|
|
|
|
tipologieOptions,
|
|
|
|
|
consegnaOptions,
|
|
|
|
|
ResetParams,
|
|
|
|
|
nFiltri,
|
|
|
|
|
} = useRicerca();
|
|
|
|
|
const mappedConsegnaOptions = useMemo(
|
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: <known lenght>
|
|
|
|
|
() => consegnaOptions.map((m) => t.preferenze.mesi[m]!).filter(Boolean),
|
|
|
|
|
[consegnaOptions, t.preferenze.mesi],
|
|
|
|
|
);
|
|
|
|
|
return (
|
2025-11-12 15:52:04 +01:00
|
|
|
<>
|
2025-11-11 17:15:29 +01:00
|
|
|
<SelectFilter
|
|
|
|
|
defaultValue={""}
|
|
|
|
|
handleReset={async () => {
|
|
|
|
|
await setTipo("");
|
|
|
|
|
}}
|
|
|
|
|
label={t.annunci.fs_tipologia.titolo}
|
|
|
|
|
onValueChange={async (value: string) => {
|
|
|
|
|
await setTipo(value);
|
|
|
|
|
}}
|
|
|
|
|
options={tipologieOptions}
|
|
|
|
|
placeholder={t.seleziona_placeholder}
|
|
|
|
|
value={tipo}
|
|
|
|
|
/>
|
|
|
|
|
<SelectFilter
|
|
|
|
|
defaultValue={""}
|
|
|
|
|
handleReset={async () => {
|
|
|
|
|
await setComune(null);
|
|
|
|
|
}}
|
|
|
|
|
label={t.annunci.comune}
|
|
|
|
|
onValueChange={async (value: string) => {
|
|
|
|
|
await setComune(value);
|
|
|
|
|
}}
|
|
|
|
|
options={comuniOptions}
|
|
|
|
|
placeholder={t.seleziona_placeholder}
|
|
|
|
|
value={comune}
|
|
|
|
|
/>
|
|
|
|
|
<SelectFilter
|
|
|
|
|
defaultValue={""}
|
|
|
|
|
handleReset={async () => {
|
|
|
|
|
await setConsegna(null);
|
|
|
|
|
}}
|
|
|
|
|
label={t.annunci.consegna}
|
|
|
|
|
onValueChange={async (value: string) => {
|
|
|
|
|
if (value === "") {
|
|
|
|
|
await setConsegna(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (t.preferenze.mesi.includes(value)) {
|
|
|
|
|
await setConsegna(t.preferenze.mesi.indexOf(value));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.warn("Value not found in preferenze.mesi");
|
|
|
|
|
await setConsegna(null);
|
|
|
|
|
}}
|
|
|
|
|
options={mappedConsegnaOptions}
|
|
|
|
|
placeholder={t.seleziona_placeholder}
|
|
|
|
|
value={consegna ? t.preferenze.mesi[consegna] || "" : ""}
|
|
|
|
|
/>
|
|
|
|
|
<SelectFilter
|
|
|
|
|
defaultValue={sort}
|
|
|
|
|
handleReset={async () => {
|
|
|
|
|
await setSort("Recenti");
|
|
|
|
|
}}
|
|
|
|
|
label={t.ordina_per}
|
|
|
|
|
onValueChange={async (value: SortTypes) => {
|
|
|
|
|
await setSort(value);
|
|
|
|
|
}}
|
|
|
|
|
options={["Recenti", "Prezzo", "Consegna", "Mq"]}
|
|
|
|
|
placeholder={t.seleziona_placeholder}
|
|
|
|
|
value={sort}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
className="flex flex-row gap-2"
|
|
|
|
|
disabled={nFiltri === 0}
|
|
|
|
|
onClick={ResetParams}
|
|
|
|
|
variant="destructive"
|
|
|
|
|
>
|
|
|
|
|
<FilterX className="size-5" /> {t.annunci.filtri_reset}
|
|
|
|
|
</Button>
|
2025-11-12 15:52:04 +01:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const RicercaSideFilters = () => {
|
|
|
|
|
return (
|
|
|
|
|
<div className="hidden h-fit w-46 max-w-46 shrink-0 grow-0 flex-col gap-3 overflow-clip rounded-md bg-white p-2 outline-neutral-300 2xl:flex">
|
|
|
|
|
<Filters />
|
2025-11-03 18:49:27 +01:00
|
|
|
</div>
|
2025-08-28 18:27:07 +02:00
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-11-11 17:15:29 +01:00
|
|
|
const DrowdownFilters = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { t } = useTranslation();
|
2025-11-12 15:52:04 +01:00
|
|
|
const { nFiltri } = useRicerca();
|
2025-08-28 18:27:07 +02:00
|
|
|
const [open, setOpen] = useState(false);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-11-11 17:15:29 +01:00
|
|
|
// Close dropdown when page resizes
|
|
|
|
|
const handleResize = useCallback(() => {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("resize", handleResize);
|
|
|
|
|
};
|
|
|
|
|
}, [handleResize]);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-12 15:52:04 +01:00
|
|
|
<div className="flex w-full sm:w-auto 2xl:hidden">
|
2025-11-11 17:15:29 +01:00
|
|
|
<DropdownMenu onOpenChange={setOpen} open={open}>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
className="group flex w-full flex-row gap-2 sm:w-auto"
|
|
|
|
|
variant="default"
|
|
|
|
|
>
|
|
|
|
|
<Badge className="w-fit rounded-md bg-secondary px-1.5 py-0.5 text-secondary-foreground dark:group-hover:bg-transparent">
|
|
|
|
|
{nFiltri}
|
|
|
|
|
</Badge>
|
|
|
|
|
<Filter className="size-5" />{" "}
|
|
|
|
|
{nFiltri === 1 ? t.annunci.filtro_attivo : t.annunci.filtri_attivi}
|
|
|
|
|
{open ? <ChevronUp /> : <ChevronDown />}
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent
|
|
|
|
|
align="start"
|
|
|
|
|
className="flex w-60 max-w-3xl flex-col gap-2 p-4"
|
|
|
|
|
>
|
2025-11-12 15:52:04 +01:00
|
|
|
<Filters />
|
2025-11-11 17:15:29 +01:00
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const RicercaHeader = () => {
|
|
|
|
|
const { map, setMap } = useRicerca();
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2025-11-12 15:52:04 +01:00
|
|
|
<div className="mx-auto w-full max-w-9xl space-y-5">
|
2025-08-28 18:27:07 +02:00
|
|
|
<div className="flex w-full flex-col-reverse gap-4 sm:flex-row sm:gap-2">
|
|
|
|
|
<div className="flex w-full gap-2 sm:w-auto">
|
2025-11-11 17:15:29 +01:00
|
|
|
<DrowdownFilters />
|
2025-08-28 18:27:07 +02:00
|
|
|
<Button
|
2025-08-29 16:18:32 +02:00
|
|
|
className="flex w-fit flex-row gap-2"
|
2025-08-28 18:27:07 +02:00
|
|
|
onClick={async () => {
|
|
|
|
|
await setMap((v) => !v);
|
|
|
|
|
}}
|
2025-08-29 16:18:32 +02:00
|
|
|
variant="default"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
{!map ? (
|
|
|
|
|
<>
|
|
|
|
|
<MapIcon className="size-5" />{" "}
|
|
|
|
|
<span className="hidden sm:block">Mappa</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Table className="size-5" />
|
|
|
|
|
<span className="hidden sm:block">Lista</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex w-full items-center justify-between gap-2 sm:justify-between">
|
2025-10-20 11:53:21 +02:00
|
|
|
<div className="relative z-35 flex w-full gap-1 sm:w-auto">
|
2025-10-20 11:56:54 +02:00
|
|
|
<CodiceBox
|
|
|
|
|
className="h-10 w-full"
|
|
|
|
|
inputId="annunci-search"
|
|
|
|
|
optionBoxClassName="top-11 "
|
|
|
|
|
/>
|
2025-08-28 18:27:07 +02:00
|
|
|
</div>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
<CTA_TipologiaModal />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const AnnunciList = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const {
|
|
|
|
|
tipo,
|
|
|
|
|
comune,
|
|
|
|
|
consegna,
|
|
|
|
|
sort,
|
|
|
|
|
map,
|
|
|
|
|
page,
|
|
|
|
|
isLoading: ricercaLoading,
|
2025-11-12 15:52:04 +01:00
|
|
|
annunciNumber,
|
2025-08-28 18:27:07 +02:00
|
|
|
} = useRicerca();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-11-12 15:52:04 +01:00
|
|
|
const {
|
|
|
|
|
data,
|
|
|
|
|
status,
|
|
|
|
|
//isPlaceholderData
|
|
|
|
|
} = api.annunci.getWithCursor.useQuery(
|
|
|
|
|
{
|
|
|
|
|
comune: comune || undefined,
|
|
|
|
|
consegna: consegna || undefined,
|
|
|
|
|
page,
|
|
|
|
|
pageSize: annunciNumber,
|
|
|
|
|
sort: sort || undefined,
|
|
|
|
|
tipologia: tipo || undefined,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
enabled: !map,
|
|
|
|
|
placeholderData: keepPreviousData,
|
|
|
|
|
staleTime: 5000,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
DISABILITATO FUNZIONALITA PAGING E PAGE SIZE
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const utils = api.useUtils();
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const prefetchData = async () => {
|
|
|
|
|
if (!isPlaceholderData && data?.hasMore) {
|
|
|
|
|
await utils.annunci.getWithCursor.prefetch({
|
|
|
|
|
comune: comune || undefined,
|
|
|
|
|
consegna: consegna || undefined,
|
2025-08-29 16:18:32 +02:00
|
|
|
page: page + 1,
|
2025-11-12 15:52:04 +01:00
|
|
|
pageSize: annunciNumber,
|
2025-08-28 18:27:07 +02:00
|
|
|
sort: sort || undefined,
|
2025-08-29 16:18:32 +02:00
|
|
|
tipologia: tipo || undefined,
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
prefetchData().catch((err) => {
|
|
|
|
|
console.error("Error prefetching next page:", err);
|
|
|
|
|
});
|
2025-11-12 15:52:04 +01:00
|
|
|
}, [data, isPlaceholderData, page, annunciNumber]);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-11-03 15:50:24 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
// Reset to page 0 if current page has no data or less than page size
|
|
|
|
|
const checkPage = async () => {
|
|
|
|
|
if (data) {
|
|
|
|
|
if (data.annunci.length === 0 && page !== 0) {
|
|
|
|
|
await setPage(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
checkPage().catch((err) => {
|
|
|
|
|
console.error("Error checking data for page reset:", err);
|
|
|
|
|
});
|
2025-11-12 15:52:04 +01:00
|
|
|
}, [data, tipo, comune, consegna, sort, setPage, annunciNumber]);
|
|
|
|
|
|
|
|
|
|
const [debouncedAnnunciNumber, setDebouncedAnnunciNumber] =
|
|
|
|
|
useState(annunciNumber);
|
|
|
|
|
|
|
|
|
|
useDebounce(
|
|
|
|
|
async () => {
|
|
|
|
|
console.log("Setting annunciNumber to", debouncedAnnunciNumber);
|
|
|
|
|
console.log("Current annunciNumber is", annunciNumber);
|
|
|
|
|
if (debouncedAnnunciNumber !== annunciNumber) {
|
|
|
|
|
await setAnnunciNumber(debouncedAnnunciNumber);
|
|
|
|
|
await setPage(0);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
500,
|
|
|
|
|
[debouncedAnnunciNumber],
|
|
|
|
|
);
|
|
|
|
|
*/
|
2025-08-28 18:27:07 +02:00
|
|
|
if (ricercaLoading) return <LoadingPage />;
|
|
|
|
|
if (status === "error") return <Status500 />;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2025-11-11 17:15:29 +01:00
|
|
|
<div className="space-y-4">
|
2025-08-28 18:27:07 +02:00
|
|
|
{status === "pending" ? (
|
|
|
|
|
<LoadingPage />
|
|
|
|
|
) : (
|
2025-11-12 15:52:04 +01:00
|
|
|
<div className="relative z-0 mx-auto grid max-w-8xl grid-flow-row grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:w-7xl 2xl:grid-cols-4">
|
|
|
|
|
{data.annunci.map((annuncio, idx) => (
|
|
|
|
|
<LazyCardAnnuncio
|
|
|
|
|
key={annuncio.codice}
|
|
|
|
|
{...annuncio}
|
|
|
|
|
eager={idx <= 9}
|
|
|
|
|
/>
|
2025-11-04 19:18:13 +01:00
|
|
|
))}
|
|
|
|
|
</div>
|
2025-08-28 18:27:07 +02:00
|
|
|
)}
|
2025-08-08 16:55:02 +02:00
|
|
|
|
2025-11-12 15:52:04 +01:00
|
|
|
{/* <div className="relative flex items-center justify-center gap-2">
|
|
|
|
|
<div className="absolute left-0 flex items-center gap-2">
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
annunci per pagina:
|
|
|
|
|
</span>
|
|
|
|
|
<NumberInput
|
|
|
|
|
allowNegative={false}
|
|
|
|
|
className="w-16 bg-white"
|
|
|
|
|
min={0}
|
|
|
|
|
onValueChange={(val) => {
|
|
|
|
|
if (val !== undefined) {
|
|
|
|
|
setDebouncedAnnunciNumber(val);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
value={debouncedAnnunciNumber}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-11-03 18:49:27 +01:00
|
|
|
<div className="flex w-24 items-center justify-end gap-2">
|
|
|
|
|
{page >= 2 && (
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Go to first page"
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
await setPage(0);
|
|
|
|
|
}}
|
|
|
|
|
title="Go to first page"
|
|
|
|
|
variant="outline"
|
|
|
|
|
>
|
|
|
|
|
<ChevronsLeft className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Previous page"
|
2025-08-29 16:18:32 +02:00
|
|
|
disabled={page === 0}
|
2025-08-28 18:27:07 +02:00
|
|
|
onClick={async () => {
|
|
|
|
|
await setPage((old) => (old ? Math.max(old - 1, 0) : 0));
|
|
|
|
|
}}
|
2025-11-03 18:49:27 +01:00
|
|
|
title="Previous page"
|
|
|
|
|
variant="outline"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
2025-11-03 18:49:27 +01:00
|
|
|
<ChevronLeft className="h-4 w-4" />
|
|
|
|
|
</Button>
|
2025-08-28 18:27:07 +02:00
|
|
|
</div>
|
2025-11-03 18:49:27 +01:00
|
|
|
<div className="mx-4 w-12 rounded-md bg-white py-2 text-center font-medium text-lg">
|
|
|
|
|
{page + 1}
|
2025-08-28 18:27:07 +02:00
|
|
|
</div>
|
2025-11-03 18:49:27 +01:00
|
|
|
<div className="flex w-24 items-center justify-start gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Next page"
|
2025-08-29 16:18:32 +02:00
|
|
|
disabled={isPlaceholderData || !data?.hasMore}
|
2025-08-28 18:27:07 +02:00
|
|
|
onClick={async () => {
|
|
|
|
|
await setPage((old) =>
|
|
|
|
|
old ? (data?.hasMore ? old + 1 : old) : 1,
|
|
|
|
|
);
|
|
|
|
|
}}
|
2025-11-03 18:49:27 +01:00
|
|
|
title="Next page"
|
|
|
|
|
variant="outline"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
2025-11-03 18:49:27 +01:00
|
|
|
<ChevronRight className="h-4 w-4" />
|
|
|
|
|
</Button>
|
2025-08-28 18:27:07 +02:00
|
|
|
</div>
|
2025-11-12 15:52:04 +01:00
|
|
|
</div> */}
|
2025-11-11 17:15:29 +01:00
|
|
|
</div>
|
2025-08-28 18:27:07 +02:00
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface SelectFilterProps<T> {
|
2025-08-28 18:27:07 +02:00
|
|
|
label: string;
|
|
|
|
|
options: string[];
|
|
|
|
|
defaultValue?: T;
|
|
|
|
|
value: T | undefined;
|
|
|
|
|
onValueChange: (value: T) => void;
|
|
|
|
|
handleReset: () => void;
|
|
|
|
|
placeholder: string;
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const SelectFilter = <T extends string>({
|
2025-08-28 18:27:07 +02:00
|
|
|
label,
|
|
|
|
|
options,
|
|
|
|
|
defaultValue,
|
|
|
|
|
value,
|
|
|
|
|
onValueChange,
|
|
|
|
|
placeholder,
|
|
|
|
|
handleReset,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: SelectFilterProps<T>) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<div className="flex h-4 flex-row gap-4 px-1">
|
|
|
|
|
<Label
|
|
|
|
|
className={cn(
|
2025-10-10 16:18:43 +02:00
|
|
|
"font-semibold text-muted-foreground",
|
2025-11-11 15:54:04 +01:00
|
|
|
value !== defaultValue && "text-primary",
|
2025-08-28 18:27:07 +02:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</Label>
|
2025-11-11 15:54:04 +01:00
|
|
|
{value !== defaultValue && (
|
2025-11-12 15:52:04 +01:00
|
|
|
<button
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
2025-08-28 18:27:07 +02:00
|
|
|
<RotateCcwIcon className="size-4" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
<Select
|
|
|
|
|
defaultValue={defaultValue}
|
|
|
|
|
onValueChange={onValueChange}
|
2025-08-29 16:18:32 +02:00
|
|
|
value={value}
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder={placeholder} />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{options.map((option) => (
|
2025-08-29 16:18:32 +02:00
|
|
|
<SelectItem className="text-left" key={option} value={option}>
|
2025-08-28 18:27:07 +02:00
|
|
|
{option}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|