This commit is contained in:
@@ -5,7 +5,6 @@ import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
||||
import IsVisible from "@/components/ui/IsVisible";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import ListWrapper from "@/components/ui/ListWrapper";
|
||||
import Pagination from "@/components/ui/pagination";
|
||||
import Status from "@/components/ui/Status";
|
||||
import {
|
||||
Table,
|
||||
@@ -49,7 +48,8 @@ const TendersTable = () => {
|
||||
navigateToTenderDetails,
|
||||
navigateToTenderFeedback,
|
||||
handlePaginationChange,
|
||||
buildWindowedPageLabel,
|
||||
hasNext,
|
||||
hasPrevious,
|
||||
getRowNumber,
|
||||
formatDateTimeCell,
|
||||
pagination,
|
||||
@@ -204,16 +204,37 @@ const TendersTable = () => {
|
||||
/>
|
||||
</div>
|
||||
<IsVisible condition={shouldShowPagination}>
|
||||
<Pagination
|
||||
key={`pagination-tenders-${absoluteCurrentPage}`}
|
||||
currentPage={pagination.currentPage}
|
||||
totalPages={pagination.totalPages}
|
||||
onPageChange={handlePaginationChange}
|
||||
pageRangeDisplayed={3}
|
||||
marginPagesDisplayed={1}
|
||||
pageLabelBuilder={buildWindowedPageLabel}
|
||||
variant="tenders"
|
||||
/>
|
||||
<nav
|
||||
className="mt-2 flex w-full items-center justify-center gap-3 text-sm"
|
||||
aria-label="Tender pagination"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasPrevious}
|
||||
onClick={() =>
|
||||
handlePaginationChange({ selected: absoluteCurrentPage - 1 })
|
||||
}
|
||||
className="shadow-theme-xs rounded-xl border border-stroke/80 bg-white/70 px-4 py-2 font-semibold text-dark transition hover:border-primary/35 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40 dark:border-dark-3 dark:bg-dark-2/70 dark:text-white"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span
|
||||
className="text-gray-600 dark:text-gray-300"
|
||||
aria-current="page"
|
||||
>
|
||||
Page {pagination.currentPage + 1}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasNext}
|
||||
onClick={() =>
|
||||
handlePaginationChange({ selected: absoluteCurrentPage + 1 })
|
||||
}
|
||||
className="shadow-theme-xs rounded-xl border border-stroke/80 bg-white/70 px-4 py-2 font-semibold text-dark transition hover:border-primary/35 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40 dark:border-dark-3 dark:bg-dark-2/70 dark:text-white"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</nav>
|
||||
</IsVisible>
|
||||
</ListWrapper>
|
||||
);
|
||||
|
||||
@@ -8,14 +8,20 @@ import { TCustomer } from "@/lib/api";
|
||||
import {
|
||||
buildQueryString,
|
||||
deleteEmptyKeys,
|
||||
getPaginatedRowNumber,
|
||||
isDateRangeActive,
|
||||
unixToDate,
|
||||
} from "@/utils/shared";
|
||||
import gsap from "gsap";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
const COMMA_SEPARATED_FILTER_KEYS = new Set([
|
||||
@@ -356,11 +362,10 @@ const useTenderListPresenter = () => {
|
||||
useEffect(() => {
|
||||
const newParams: Record<string, any> = {
|
||||
...apiDefaultParams,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
limit: 20,
|
||||
};
|
||||
for (const key of searchParams.keys()) {
|
||||
if (key === "cpv_codes") {
|
||||
if (key === "cpv_codes" || key === "include_total" || key === "cursor") {
|
||||
continue;
|
||||
}
|
||||
newParams[key] = searchParams.get(key);
|
||||
@@ -376,53 +381,21 @@ const useTenderListPresenter = () => {
|
||||
|
||||
const { data, error, isPending, isError } = useTendersQuery(params);
|
||||
|
||||
const { pagination, handlePaginationChange, buildFirstPageParams } =
|
||||
useHybridPagination({
|
||||
meta: data?.meta,
|
||||
params,
|
||||
setParams,
|
||||
isError,
|
||||
error,
|
||||
});
|
||||
const {
|
||||
pagination,
|
||||
handlePaginationChange,
|
||||
buildFirstPageParams,
|
||||
hasNext,
|
||||
hasPrevious,
|
||||
} = useHybridPagination({
|
||||
meta: data?.meta,
|
||||
params,
|
||||
setParams,
|
||||
isError,
|
||||
error,
|
||||
});
|
||||
|
||||
const MAX_PAGE_JUMP = 10;
|
||||
const paginationWindow = useMemo(() => {
|
||||
const windowStart = Math.max(0, pagination.currentPage - MAX_PAGE_JUMP);
|
||||
const windowEnd = Math.min(
|
||||
Math.max(pagination.totalPages - 1, 0),
|
||||
pagination.currentPage + MAX_PAGE_JUMP,
|
||||
);
|
||||
return {
|
||||
windowStart,
|
||||
virtualCurrent: pagination.currentPage - windowStart,
|
||||
virtualTotal: Math.max(windowEnd - windowStart + 1, 1),
|
||||
};
|
||||
}, [pagination.currentPage, pagination.totalPages]);
|
||||
|
||||
const handleWindowedPaginationChange = useCallback(
|
||||
(e: { selected: number }) => {
|
||||
handlePaginationChange({
|
||||
selected: e.selected + paginationWindow.windowStart,
|
||||
});
|
||||
},
|
||||
[handlePaginationChange, paginationWindow.windowStart],
|
||||
);
|
||||
|
||||
const buildWindowedPageLabel = useCallback(
|
||||
(virtualPage: number) => String(virtualPage + paginationWindow.windowStart),
|
||||
[paginationWindow.windowStart],
|
||||
);
|
||||
|
||||
const windowedPagination = useMemo(
|
||||
() => ({
|
||||
currentPage: paginationWindow.virtualCurrent,
|
||||
totalPages: paginationWindow.virtualTotal,
|
||||
}),
|
||||
[paginationWindow.virtualCurrent, paginationWindow.virtualTotal],
|
||||
);
|
||||
|
||||
const shouldShowPagination =
|
||||
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
|
||||
const shouldShowPagination = !isPending && (hasPrevious || hasNext);
|
||||
|
||||
const tendersList = data?.data?.tenders ?? [];
|
||||
|
||||
@@ -449,12 +422,11 @@ const useTenderListPresenter = () => {
|
||||
|
||||
const getRowNumber = useCallback(
|
||||
(index: number) =>
|
||||
getPaginatedRowNumber({
|
||||
index,
|
||||
page: data?.meta?.page,
|
||||
limit: data?.meta?.limit,
|
||||
}),
|
||||
[data?.meta?.limit, data?.meta?.page],
|
||||
pagination.currentPage *
|
||||
(data?.meta?.limit ?? Number(params.limit) ?? 20) +
|
||||
index +
|
||||
1,
|
||||
[data?.meta?.limit, pagination.currentPage, params.limit],
|
||||
);
|
||||
|
||||
const formatDateTimeCell = useCallback((unix: number) => {
|
||||
@@ -541,7 +513,7 @@ const useTenderListPresenter = () => {
|
||||
const clearAllFilters = useCallback(() => {
|
||||
const cleared = buildFirstPageParams({
|
||||
...apiDefaultParams,
|
||||
limit: 10,
|
||||
limit: 20,
|
||||
});
|
||||
filterFormReset(mergeTenderFilterFormState(cleared));
|
||||
router.push(pathName);
|
||||
@@ -629,11 +601,12 @@ const useTenderListPresenter = () => {
|
||||
tendersList,
|
||||
navigateToTenderDetails,
|
||||
navigateToTenderFeedback,
|
||||
handlePaginationChange: handleWindowedPaginationChange,
|
||||
buildWindowedPageLabel,
|
||||
handlePaginationChange,
|
||||
hasNext,
|
||||
hasPrevious,
|
||||
getRowNumber,
|
||||
formatDateTimeCell,
|
||||
pagination: windowedPagination,
|
||||
pagination,
|
||||
absoluteCurrentPage: pagination.currentPage,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { API_ENDPOINTS, tendersService } from "@/lib/api";
|
||||
import { TTenderListParams } from "@/lib/api/types/Tenders";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useTendersQuery = (params?: Record<string, any>) => {
|
||||
export const useTendersQuery = (params?: TTenderListParams) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.TENDERS.READ_ALL, "read-tenders", params],
|
||||
[params],
|
||||
@@ -15,7 +16,10 @@ export const useTendersQuery = (params?: Record<string, any>) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useTenderDetailQuery = (id: string, params?: Record<string, any>) => {
|
||||
export const useTenderDetailQuery = (
|
||||
id: string,
|
||||
params?: Record<string, any>,
|
||||
) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.TENDERS.DETAILS(id), params],
|
||||
[id, params],
|
||||
@@ -52,13 +56,10 @@ export const useGetTenderDocumentsQuery = (
|
||||
id: string,
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.TENDERS.DOCUMENTS(id)],
|
||||
[id],
|
||||
);
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.TENDERS.DOCUMENTS(id)], [id]);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => tendersService.getDocuments(id),
|
||||
enabled: options?.enabled ?? true,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { IMetaData } from "@/lib/api/types/Factory";
|
||||
import { AxiosError } from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
@@ -12,7 +11,14 @@ const isInvalidCursorError = (error: unknown) => {
|
||||
};
|
||||
|
||||
type UseHybridPaginationOptions = {
|
||||
meta?: IMetaData;
|
||||
meta?: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
page?: number;
|
||||
pages?: number;
|
||||
has_more?: boolean;
|
||||
next_cursor?: string;
|
||||
};
|
||||
params: Record<string, unknown>;
|
||||
setParams: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
isError?: boolean;
|
||||
@@ -91,7 +97,14 @@ export const useHybridPagination = ({
|
||||
|
||||
if (meta?.page != null) return meta.page - 1;
|
||||
return 0;
|
||||
}, [cursorStack, params.cursor, params.offset, meta?.offset, meta?.page, limit]);
|
||||
}, [
|
||||
cursorStack,
|
||||
params.cursor,
|
||||
params.offset,
|
||||
meta?.offset,
|
||||
meta?.page,
|
||||
limit,
|
||||
]);
|
||||
|
||||
const hasPrevious = cursorStack.length > 0 || currentPageIndex > 0;
|
||||
|
||||
@@ -129,7 +142,11 @@ export const useHybridPagination = ({
|
||||
{ cursor: nextCursor, fromPage: fromPage1Based },
|
||||
]);
|
||||
setParams((current) => {
|
||||
const next: Record<string, any> = { ...current, cursor: nextCursor, limit };
|
||||
const next: Record<string, any> = {
|
||||
...current,
|
||||
cursor: nextCursor,
|
||||
limit,
|
||||
};
|
||||
delete next.offset;
|
||||
return next;
|
||||
});
|
||||
|
||||
@@ -5,14 +5,16 @@ import { ApiResponse } from "../types";
|
||||
import {
|
||||
TTenderDetails,
|
||||
TTenderDocumentsResponse,
|
||||
TTenderListParams,
|
||||
TTenderListMeta,
|
||||
TTenderResponse,
|
||||
TTenderTranslationResponse,
|
||||
} from "../types/Tenders";
|
||||
|
||||
export const tendersService = {
|
||||
tendersList: async (
|
||||
params?: Record<string, any>,
|
||||
): Promise<ApiResponse<TTenderResponse>> => {
|
||||
params?: TTenderListParams,
|
||||
): Promise<ApiResponse<TTenderResponse, TTenderListMeta>> => {
|
||||
try {
|
||||
return (
|
||||
await api.get(API_ENDPOINTS.TENDERS.READ_ALL, {
|
||||
@@ -30,7 +32,8 @@ export const tendersService = {
|
||||
params?: Record<string, any>,
|
||||
): Promise<ApiResponse<TTenderDetails>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id), { params })).data;
|
||||
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id), { params }))
|
||||
.data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR caught in Tender Service => tender details: ",
|
||||
@@ -53,12 +56,17 @@ export const tendersService = {
|
||||
})
|
||||
).data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in Tender Service => translate tender: ", error);
|
||||
console.error(
|
||||
"ERROR caught in Tender Service => translate tender: ",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getDocuments: async (id: string): Promise<ApiResponse<TTenderDocumentsResponse[]>> => {
|
||||
getDocuments: async (
|
||||
id: string,
|
||||
): Promise<ApiResponse<TTenderDocumentsResponse[]>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.TENDERS.DOCUMENTS(id))).data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -123,9 +123,33 @@ export const TendersResponse = z.object({
|
||||
tenders: z.array(TenderSchema).or(z.null()),
|
||||
});
|
||||
|
||||
export type TTenderDocumentsResponse = z.infer<typeof TenderDocumentsResponseSchema>;
|
||||
export type TTenderDocumentsResponse = z.infer<
|
||||
typeof TenderDocumentsResponseSchema
|
||||
>;
|
||||
export type TTenderResponse = z.infer<typeof TendersResponse>;
|
||||
export type TTenderDetails = z.infer<typeof TenderSchema>;
|
||||
export interface TTenderListParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
cursor?: string;
|
||||
include_total?: boolean;
|
||||
sort_by?: string;
|
||||
sort_order?: "asc" | "desc";
|
||||
q?: string;
|
||||
status?: string[];
|
||||
country?: string;
|
||||
documents_scraped?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface TTenderListMeta {
|
||||
limit: number;
|
||||
offset: number;
|
||||
has_more: boolean;
|
||||
next_cursor?: string;
|
||||
total?: number;
|
||||
page?: number;
|
||||
pages?: number;
|
||||
}
|
||||
export type TTenderTranslationResponse = z.infer<
|
||||
typeof TenderTranslationResponseSchema
|
||||
>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IMetaData } from "./Factory";
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
export interface ApiResponse<T, TMeta = IMetaData> {
|
||||
data: T;
|
||||
message: string;
|
||||
code?: number;
|
||||
meta?: IMetaData;
|
||||
meta?: TMeta;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user