"use client"; import { IMetaData } from "@/lib/api/types/Factory"; import { AxiosError } from "axios"; import { useCallback, useEffect, useMemo, useState } from "react"; const isInvalidCursorError = (error: unknown) => { const axiosError = error as AxiosError<{ error?: { message?: string } }>; if (axiosError.response?.status !== 400) return false; const message = axiosError.response?.data?.error?.message ?? ""; return /cursor|pagination/i.test(message); }; type UseHybridPaginationOptions = { meta?: IMetaData; params: Record; setParams: React.Dispatch>>; isError?: boolean; error?: unknown; }; export const useHybridPagination = ({ meta, params, setParams, isError, error, }: UseHybridPaginationOptions) => { const [cursorStack, setCursorStack] = useState([]); const limit = meta?.limit ?? Number(params.limit) ?? 10; const clearCursorStack = useCallback(() => { setCursorStack([]); }, []); const resetToFirstPage = useCallback(() => { clearCursorStack(); setParams((current) => { const next: Record = { ...current, offset: 0, limit }; delete next.cursor; return next; }); }, [clearCursorStack, limit, setParams]); useEffect(() => { if (!isError || !error || !isInvalidCursorError(error)) return; resetToFirstPage(); }, [isError, error, resetToFirstPage]); const hasNext = Boolean(meta?.has_more && meta?.next_cursor); const hasPrevious = cursorStack.length > 0 || (meta?.page ?? 1) > 1; const currentPageIndex = useMemo(() => { if (meta?.page != null) return meta.page - 1; return cursorStack.length; }, [cursorStack.length, meta?.page]); const pagination = useMemo( () => ({ currentPage: currentPageIndex, totalPages: meta?.pages ?? 1, }), [currentPageIndex, meta?.pages], ); const goToOffsetPage = useCallback( (pageIndex: number) => { clearCursorStack(); setParams((current) => { const next: Record = { ...current, offset: pageIndex * limit, limit, }; delete next.cursor; return next; }); }, [clearCursorStack, limit, setParams], ); const goToNextCursorPage = useCallback(() => { const nextCursor = meta?.next_cursor; if (!meta?.has_more || !nextCursor) return; setCursorStack((stack) => [...stack, nextCursor]); setParams((current) => { const next: Record = { ...current, cursor: nextCursor, limit }; delete next.offset; return next; }); }, [limit, meta?.has_more, meta?.next_cursor, setParams]); const goToPreviousPage = useCallback(() => { if (cursorStack.length > 0) { const nextStack = cursorStack.slice(0, -1); const previousCursor = nextStack[nextStack.length - 1]; setCursorStack(nextStack); setParams((current) => { const next: Record = { ...current, limit }; if (previousCursor) { next.cursor = previousCursor; delete next.offset; } else { next.offset = 0; delete next.cursor; } return next; }); return; } const page = meta?.page ?? 1; if (page <= 1) { resetToFirstPage(); return; } goToOffsetPage(page - 2); }, [cursorStack, goToOffsetPage, limit, meta?.page, resetToFirstPage, setParams]); const handlePaginationChange = useCallback( (e: { selected: number }) => { const targetIndex = e.selected; const delta = targetIndex - currentPageIndex; if (delta === 1 && hasNext) { goToNextCursorPage(); return; } if (delta === -1 && hasPrevious) { goToPreviousPage(); return; } goToOffsetPage(targetIndex); }, [ currentPageIndex, goToNextCursorPage, goToOffsetPage, goToPreviousPage, hasNext, hasPrevious, ], ); const buildFirstPageParams = useCallback( (base: Record) => { const next: Record = { ...base, offset: 0, limit }; delete next.cursor; clearCursorStack(); return next; }, [clearCursorStack, limit], ); return { pagination, handlePaginationChange, buildFirstPageParams, resetToFirstPage, clearCursorStack, currentPageIndex, hasNext, hasPrevious, }; };