Refactor pagination logic in AnnunciList and update API to use page and pageSize parameters for improved data fetching

This commit is contained in:
Marco Pedone 2025-08-08 16:55:02 +02:00
parent c574af9096
commit 9b44cad8eb
5 changed files with 132 additions and 216 deletions

View file

@ -73,7 +73,10 @@ const MyApp = ({
`}</style>
<Toaster position="top-right" reverseOrder={false} />
<NextNProgress options={{ showSpinner: false }} />
{!router.pathname.startsWith("/annunci") && (
<NextNProgress options={{ showSpinner: false }} />
)}
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</SessionProvider>

View file

@ -29,10 +29,7 @@ import { Status500 } from "~/components/status-page";
import { CodiceBox } from "~/components/codiceRicerca";
import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { MapSection } from "~/components/annunci_map";
import { api } from "~/utils/api";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import {
DropdownMenu,
DropdownMenuContent,
@ -47,6 +44,7 @@ import {
} from "~/providers/RicercaProvider";
import { AccordionComp } from "~/components/accordionComp";
import { CTA_TipologiaModal } from "~/components/annunci_tutorial";
import { keepPreviousData } from "@tanstack/react-query";
type AnnunciPageProps = {
options: AnnunciOptions;
@ -265,7 +263,6 @@ const RicercaFilters = () => {
};
const AnnunciList = () => {
const router = useRouter();
const {
tipo,
comune,
@ -274,68 +271,110 @@ const AnnunciList = () => {
map,
page,
setPage,
lastPageCount,
setlastPageCount,
isLoading: ricercaLoading,
} = useRicerca();
const { data, fetchNextPage, isLoading, hasNextPage } =
api.annunci.getWithCursor.useInfiniteQuery(
const { data, status, isPlaceholderData } =
api.annunci.getWithCursor.useQuery(
{
limit: 6,
page,
pageSize: 6,
tipologia: tipo || undefined,
comune: comune || undefined,
consegna: consegna || undefined,
sort: sort || undefined,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: keepPreviousData,
enabled: !map,
staleTime: 5000,
},
);
const handlePageReset = () => {
setPage(0);
};
const handleFetchNextPage = async () => {
try {
await fetchNextPage();
setPage((prev) => prev + 1);
} catch {
await router.push("/annunci");
toast.error("Errore caricamento annunci");
}
};
const handleFetchPreviousPage = () => {
setPage((prev) => prev - 1);
};
const utils = api.useUtils();
useEffect(() => {
if (
hasNextPage !== undefined &&
hasNextPage === false &&
lastPageCount === 100
) {
setlastPageCount(page);
}
}, [hasNextPage, page, lastPageCount]);
const prefetchData = async () => {
if (!isPlaceholderData && data?.hasMore) {
await utils.annunci.getWithCursor.prefetch({
page: page + 1,
pageSize: 6,
tipologia: tipo || undefined,
comune: comune || undefined,
consegna: consegna || undefined,
sort: sort || undefined,
});
}
};
prefetchData().catch((err) => {
console.error("Error prefetching next page:", err);
});
}, [data, isPlaceholderData, page]);
if (isLoading || ricercaLoading) return <LoadingPage />;
if ((!isLoading && !data) || !data) return <Status500 />;
if (ricercaLoading) return <LoadingPage />;
if (status === "error") return <Status500 />;
return (
<>
<AnnunciGrid pagedata={data.pages[page]!.annunci} />
{status === "pending" ? (
<LoadingPage />
) : (
<AnnunciGrid pagedata={data.annunci} />
)}
<br />
<PaginationHandler
handleFetchNextPage={handleFetchNextPage}
handleFetchPreviousPage={handleFetchPreviousPage}
handlePageReset={handlePageReset}
page={page}
hasNextPage={data?.pages[page]?.nextCursor}
/>
<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",
)}
onClick={async () => {
await setPage(0);
}}
disabled={page < 2}
>
<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",
)}
onClick={async () => {
await setPage((old) => (old ? Math.max(old - 1, 0) : 0));
}}
disabled={page === 0}
>
<ChevronRight height={30} className="rotate-180" />
</button>
</div>
<div className="flex items-center justify-center">
<span className="text-lg font-semibold text-neutral-500">
{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",
)}
onClick={async () => {
await setPage((old) =>
old ? (data?.hasMore ? old + 1 : old) : 1,
);
}}
disabled={isPlaceholderData || !data?.hasMore}
>
<ChevronRight height={30} />
</button>
</div>
</div>
</>
);
};
@ -396,73 +435,3 @@ export const SelectFilter = <T extends string>({
</div>
);
};
const PaginationHandler = ({
handleFetchNextPage,
handleFetchPreviousPage,
handlePageReset,
page,
hasNextPage,
}: {
handleFetchNextPage: () => Promise<void>;
handleFetchPreviousPage: () => void;
handlePageReset: () => void;
page: number;
hasNextPage: string | undefined;
}) => {
const [attivo, setAttivo] = useState(true);
const fetchNextPage = async () => {
setAttivo(false);
await handleFetchNextPage();
setAttivo(true);
};
return (
<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",
)}
onClick={handlePageReset}
disabled={page < 2}
>
<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",
)}
onClick={handleFetchPreviousPage}
disabled={page < 1}
>
<ChevronRight height={30} className="rotate-180" />
</button>
</div>
<div className="flex items-center justify-center">
<span className="text-lg font-semibold text-neutral-500">
{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",
hasNextPage === undefined ? "invisible" : "visible",
)}
onClick={fetchNextPage}
disabled={!hasNextPage || attivo === false}
>
<ChevronRight height={30} />
</button>
</div>
</div>
);
};

View file

@ -3,12 +3,9 @@ import {
createContext,
useContext,
useMemo,
useState,
useTransition,
type Dispatch,
type FC,
type ReactNode,
type SetStateAction,
} from "react";
import {
parseAsBoolean,
@ -45,9 +42,9 @@ type RicercaContextType = {
consegnaOptions: number[];
sortOptions: typeof sortOptions;
page: number;
setPage: Dispatch<SetStateAction<number>>;
lastPageCount: number;
setlastPageCount: Dispatch<SetStateAction<number>>;
setPage: (
value: number | ((old: number | null) => number | null) | null,
) => Promise<URLSearchParams>;
ResetParams: () => Promise<void>;
nFiltri: number;
isLoading: boolean;
@ -103,8 +100,10 @@ export const RicercaProvider: FC<{
.withOptions({ startTransition }),
);
const [page, setPage] = useState(0);
const [lastPageCount, setlastPageCount] = useState(100);
const [page, setPage] = useQueryState(
"p",
parseAsInteger.withDefault(0).withOptions({ clearOnDefault: true }),
);
const tipologieOptions = useMemo(() => {
const set = new Set<string>();
@ -147,7 +146,7 @@ export const RicercaProvider: FC<{
await setComune("");
await setConsegna(null);
await setSort("");
setPage(0);
await setPage(0);
};
const nFiltri =
@ -176,8 +175,6 @@ export const RicercaProvider: FC<{
page,
setPage,
lastPageCount,
setlastPageCount,
ResetParams,
nFiltri,
isLoading,

View file

@ -57,8 +57,8 @@ export const annunciRouter = createTRPCRouter({
getWithCursor: publicProcedure
.input(
z.object({
limit: z.number().min(1).max(100).optional(),
cursor: z.string().optional(),
page: z.number().optional(),
pageSize: z.number(),
tipologia: z.string().optional(),
comune: z.string().optional(),
consegna: z.number().optional(),
@ -66,13 +66,9 @@ export const annunciRouter = createTRPCRouter({
}),
)
.query(async ({ input }) => {
const { annunci, nextCursor } = await getCursor_AnnunciHandler({
input,
return await getCursor_AnnunciHandler({
...input,
});
return {
annunci,
nextCursor,
};
}),
getMapping: publicProcedure
.input(

View file

@ -233,34 +233,20 @@ export type AnnuncioRicerca = Pick<
>;
export const getCursor_AnnunciHandler = async ({
input,
page = 0,
pageSize,
tipologia,
comune,
consegna,
sort,
}: {
input: {
limit?: number;
cursor?: string;
tipologia?: string;
comune?: string;
consegna?: number;
sort?: "Recenti" | "Prezzo" | "Consegna" | "Mq";
};
}): Promise<{
annunci: AnnuncioRicerca[];
nextCursor: string | undefined;
}> => {
const limit = input.limit ?? 50;
const [cursor, sortCursor] = input.cursor?.split("_") ?? [0, undefined];
function sortType(sort: "Recenti" | "Prezzo" | "Consegna" | "Mq") {
switch (sort) {
case "Recenti":
return { col: "modificato_il", opr: "desc" };
case "Prezzo":
return { col: "prezzo", opr: "asc" };
case "Consegna":
return { col: "consegna", opr: "asc" };
case "Mq":
return { col: "mq", opr: "asc" };
}
}
page?: number;
pageSize: number;
tipologia?: string;
comune?: string;
consegna?: number;
sort?: "Recenti" | "Prezzo" | "Consegna" | "Mq";
}) => {
try {
let query = db
.selectFrom("annunci")
@ -283,35 +269,18 @@ export const getCursor_AnnunciHandler = async ({
.where("web", "=", true)
.where("stato", "!=", "Sospeso");
if (sortCursor && input.sort !== undefined) {
const { col: sortColumn, opr } = sortType(input.sort) as {
col: keyof Annunci;
opr: string;
};
query = query.where((eb) =>
eb.or([
eb.and([
eb("id", ">=", (cursor as AnnunciId) ?? 0),
eb(sortColumn, "=", sortCursor.toString()),
]),
eb(sortColumn, opr === "desc" ? "<" : ">", sortCursor.toString()),
]),
);
} else {
query = query.where("id", ">=", (cursor as AnnunciId) ?? 0);
}
if (input.tipologia && input.tipologia !== "") {
query = query.where("tipo", "=", input.tipologia);
if (tipologia && tipologia !== "") {
query = query.where("tipo", "=", tipologia);
}
if (input.comune && input.comune !== "") {
query = query.where("comune", "like", input.comune);
if (comune && comune !== "") {
query = query.where("comune", "like", comune);
}
if (input.consegna) {
query = query.where("consegna", "=", input.consegna);
if (consegna) {
query = query.where("consegna", "=", consegna);
}
if (input.sort) {
switch (input.sort) {
if (sort) {
switch (sort) {
case "Recenti":
query = query.orderBy("modificato_il", "desc");
break;
@ -328,32 +297,13 @@ export const getCursor_AnnunciHandler = async ({
}
query = query.orderBy("id", "asc");
const itemsraw = await query.limit(limit + 1).execute();
const annunci = itemsraw;
let nextCursor: string | undefined = undefined;
if (annunci.length > limit) {
const nextItem = annunci.pop(); // return the last item from the array
if (!nextItem) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore interno",
});
}
if (!input.sort) {
nextCursor = nextItem.id.toString();
} else {
const { col: sortColumn } = sortType(input.sort);
if (sortColumn === "modificato_il") {
if (!nextItem.modificato_il) {
nextCursor = `${nextItem.id.toString()}_1990-01-01`;
} else {
nextCursor = `${nextItem.id.toString()}_${nextItem.modificato_il.toISOString()}`;
}
} else {
nextCursor = `${nextItem.id.toString()}_${nextItem[sortColumn as keyof typeof nextItem]?.toString()}`;
}
}
const annunci = await query
.limit(pageSize + 1)
.offset(page * pageSize)
.execute();
const hasMore = annunci.length > pageSize;
if (annunci.length > pageSize) {
annunci.pop(); // Remove the last item if it exceeds the page size
}
for (const annuncio of annunci) {
@ -361,9 +311,10 @@ export const getCursor_AnnunciHandler = async ({
? createSrcset(annuncio.url_immagini)
: [];
}
return {
annunci,
nextCursor,
hasMore,
};
} catch {
throw new TRPCError({