infoalloggi-monorepo/apps/infoalloggi/src/pages/annunci.tsx

460 lines
12 KiB
TypeScript
Raw Normal View History

2025-08-28 18:27:07 +02:00
import { keepPreviousData } from "@tanstack/react-query";
import {
ChevronDown,
ChevronRight,
ChevronsRight,
ChevronUp,
Filter,
FilterX,
MapIcon,
RotateCcwIcon,
Table,
} from "lucide-react";
2025-08-04 17:45:44 +02:00
import type { GetServerSideProps } from "next";
import Head from "next/head";
import { useEffect, useMemo, useState } from "react";
2025-08-28 18:27:07 +02:00
import { AccordionComp } from "~/components/accordionComp";
import { AnnunciGrid } from "~/components/annunci_grid";
import { MapSection } from "~/components/annunci_map";
import { CTA_TipologiaModal } from "~/components/annunci_tutorial";
import { CodiceBox } from "~/components/codiceRicerca";
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
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
<main className="mx-auto w-full max-w-7xl space-y-5 bg-background2 px-2 py-5 md:px-8">
2025-10-10 16:18:43 +02:00
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
{t.annunci.titolo}
</div>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<Ricerca />
<AccordionComp
className="max-w-6xl px-4 py-4 md:py-8"
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
};
export default Annunci;
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 (
<>
<RicercaFilters />
<br />
{map ? <MapSection /> : <AnnunciList />}
</>
);
2025-08-04 17:45:44 +02:00
};
const RicercaFilters = () => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
const {
map,
setMap,
tipo,
setTipo,
comune,
setComune,
consegna,
setConsegna,
sort,
setSort,
comuniOptions,
tipologieOptions,
consegnaOptions,
ResetParams,
nFiltri,
} = useRicerca();
const [open, setOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const mappedConsegnaOptions = useMemo(
// biome-ignore lint/style/noNonNullAssertion: <known lenght>
() => consegnaOptions.map((m) => t.preferenze.mesi[m]!).filter(Boolean),
[consegnaOptions, t.preferenze.mesi],
);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<div className="mx-auto w-full max-w-7xl 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">
<div className="flex w-full sm:w-auto">
2025-08-29 16:18:32 +02:00
<DropdownMenu onOpenChange={setOpen} open={open}>
2025-08-28 18:27:07 +02:00
<DropdownMenuTrigger asChild>
<Button
className="group flex w-full flex-row gap-2 sm:w-auto"
2025-08-29 16:18:32 +02:00
variant="default"
2025-08-28 18:27:07 +02:00
>
2025-10-10 16:18:43 +02:00
<Badge className="w-fit rounded-md bg-secondary px-1.5 py-0.5 text-secondary-foreground dark:group-hover:bg-transparent">
2025-08-28 18:27:07 +02:00
{nFiltri}
</Badge>
<Filter className="size-5" />{" "}
{nFiltri === 1
? t.annunci.filtro_attivo
: t.annunci.filtri_attivi}
{open ? <ChevronUp /> : <ChevronDown />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
2025-08-29 16:18:32 +02:00
className="flex w-60 max-w-3xl flex-col gap-2 p-4"
2025-08-28 18:27:07 +02:00
>
<SelectFilter
defaultValue={tipo}
2025-08-29 16:18:32 +02:00
handleReset={async () => {
await setTipo("");
}}
label={t.annunci.fs_tipologia.titolo}
2025-08-28 18:27:07 +02:00
onValueChange={async (value: string) => {
await setTipo(value);
}}
2025-08-29 16:18:32 +02:00
options={tipologieOptions}
2025-08-28 18:27:07 +02:00
placeholder={t.seleziona_placeholder}
2025-08-29 16:18:32 +02:00
value={tipo}
2025-08-28 18:27:07 +02:00
/>
<SelectFilter
defaultValue={comune}
2025-08-29 16:18:32 +02:00
handleReset={async () => {
await setComune(null);
}}
label={t.annunci.comune}
2025-08-28 18:27:07 +02:00
onValueChange={async (value: string) => {
await setComune(value);
}}
2025-08-29 16:18:32 +02:00
options={comuniOptions}
2025-08-28 18:27:07 +02:00
placeholder={t.seleziona_placeholder}
2025-08-29 16:18:32 +02:00
value={comune}
2025-08-28 18:27:07 +02:00
/>
<SelectFilter
defaultValue={
consegna ? t.preferenze.mesi[consegna] || "" : ""
}
2025-08-29 16:18:32 +02:00
handleReset={async () => {
await setConsegna(null);
}}
label={t.annunci.consegna}
2025-08-28 18:27:07 +02:00
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);
2025-08-28 18:27:07 +02:00
}}
2025-08-29 16:18:32 +02:00
options={mappedConsegnaOptions}
2025-08-28 18:27:07 +02:00
placeholder={t.seleziona_placeholder}
2025-08-29 16:18:32 +02:00
value={consegna ? t.preferenze.mesi[consegna] || "" : ""}
2025-08-28 18:27:07 +02:00
/>
<SelectFilter
defaultValue={sort}
2025-08-29 16:18:32 +02:00
handleReset={async () => {
await setSort("");
}}
label={t.ordina_per}
2025-08-28 18:27:07 +02:00
onValueChange={async (value: SortTypes) => {
await setSort(value);
}}
2025-08-29 16:18:32 +02:00
options={["Recenti", "Prezzo", "Consegna", "Mq"]}
2025-08-28 18:27:07 +02:00
placeholder={t.seleziona_placeholder}
2025-08-29 16:18:32 +02:00
value={sort}
2025-08-28 18:27:07 +02:00
/>
<Button
className="flex flex-row gap-2"
disabled={nFiltri === 0}
2025-08-29 16:18:32 +02:00
onClick={ResetParams}
variant="destructive"
2025-08-28 18:27:07 +02:00
>
<FilterX className="size-5" /> {t.annunci.filtri_reset}
</Button>
</DropdownMenuContent>
</DropdownMenu>
</div>
<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">
<div className="relative z-35 flex w-full gap-1 sm:w-auto">
<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,
setPage,
isLoading: ricercaLoading,
} = useRicerca();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { data, status, isPlaceholderData } =
api.annunci.getWithCursor.useQuery(
{
comune: comune || undefined,
consegna: consegna || undefined,
2025-08-29 16:18:32 +02:00
page,
pageSize: 6,
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
},
{
enabled: !map,
2025-08-29 16:18:32 +02:00
placeholderData: keepPreviousData,
2025-08-28 18:27:07 +02:00
staleTime: 5000,
},
);
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,
pageSize: 6,
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);
});
}, [data, isPlaceholderData, page]);
2025-08-04 17:45:44 +02: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);
}
if (data.annunci.length < 6 || !data.hasMore) {
await setPage(0);
}
}
};
checkPage().catch((err) => {
console.error("Error checking data for page reset:", err);
});
}, [data, tipo, comune, consegna, sort, setPage]);
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 (
<>
{status === "pending" ? (
<LoadingPage />
) : (
<AnnunciGrid pagedata={data.annunci} />
)}
<br />
2025-08-28 18:27:07 +02:00
<div className="mx-auto grid max-w-sm grid-cols-5 items-center justify-between gap-2">
<div className="flex items-center justify-center">
<button
aria-label="Reset Page"
className={cn(
"rounded-lg bg-red-500/80 px-4 py-2 text-white hover:bg-red-500 active:shadow-lg",
page < 2 ? "invisible" : "visible",
)}
2025-08-29 16:18:32 +02:00
disabled={page < 2}
2025-08-28 18:27:07 +02:00
onClick={async () => {
await setPage(0);
}}
2025-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
>
<ChevronsRight className="rotate-180" height={30} />
</button>
</div>
<div className="flex items-center justify-center">
<button
aria-label="Previous Page"
className={cn(
"rounded-lg bg-red-500/80 px-4 py-2 text-white hover:bg-red-500 active:shadow-lg",
page < 1 ? "invisible" : "visible",
)}
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-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
>
2025-08-29 16:18:32 +02:00
<ChevronRight className="rotate-180" height={30} />
2025-08-28 18:27:07 +02:00
</button>
</div>
<div className="flex items-center justify-center">
2025-10-10 16:18:43 +02:00
<span className="font-semibold text-lg text-neutral-500">
2025-08-28 18:27:07 +02:00
{page + 1}
</span>
</div>
<div className="flex items-center justify-center">
<button
aria-label="Next Page"
className={cn(
"rounded-lg bg-red-500/80 px-4 py-2 text-white hover:bg-red-500 active:shadow-lg",
data?.hasMore ? "visible" : "invisible",
)}
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-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
>
<ChevronRight height={30} />
</button>
</div>
</div>
</>
);
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-08-28 18:27:07 +02:00
value !== "" && "text-primary",
)}
>
{label}
</Label>
{value !== "" && (
2025-08-29 16:18:32 +02:00
<button 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
};