mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-13 20:15:46 +01:00
fix: comprehensive TypeScript/build fixes and modernization
- Update tsconfig to ES2024 target and bundler moduleResolution - Add dynamic imports for chart.js and recharts (bundle optimization) - Consolidate 17 useState into useReducer in sessions page - Fix 18 .js extension imports across lib files - Add type declarations for @rapideditor/country-coder - Fix platform user types (PlatformUserRole enum) - Fix Calendar component prop types - Centralize next-auth type augmentation - Add force-dynamic to all API routes (prevent build-time prerender) - Fix Prisma JSON null handling with Prisma.DbNull - Fix various type mismatches (SessionMessage, ImportRecord, etc.) - Export ButtonProps from button component - Update next-themes import path - Replace JSX.Element with React.ReactElement - Remove obsolete debug scripts and pnpm lockfile - Downgrade eslint to v8 for next compatibility
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
import { useCallback, useEffect, useId, useReducer } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -22,21 +22,132 @@ import { Label } from "@/components/ui/label";
|
||||
import { formatCategory } from "@/lib/format-enums";
|
||||
import type { ChatSession } from "../../../lib/types";
|
||||
|
||||
// Placeholder for a SessionListItem component to be created later
|
||||
// For now, we'll display some basic info directly.
|
||||
// import SessionListItem from "../../../components/SessionListItem";
|
||||
// State types
|
||||
interface FilterState {
|
||||
searchTerm: string;
|
||||
debouncedSearchTerm: string;
|
||||
category: string;
|
||||
language: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
sortKey: string;
|
||||
sortOrder: "asc" | "desc";
|
||||
options: { categories: string[]; languages: string[] };
|
||||
}
|
||||
|
||||
// TODO: Consider moving filter/sort types to lib/types.ts if they become complex
|
||||
interface FilterOptions {
|
||||
categories: string[];
|
||||
languages: string[];
|
||||
interface PaginationState {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
interface UIState {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
filtersExpanded: boolean;
|
||||
}
|
||||
|
||||
interface SessionsState {
|
||||
sessions: ChatSession[];
|
||||
filters: FilterState;
|
||||
pagination: PaginationState;
|
||||
ui: UIState;
|
||||
}
|
||||
|
||||
// Action types
|
||||
type SessionsAction =
|
||||
| { type: "SET_SESSIONS"; payload: ChatSession[] }
|
||||
| {
|
||||
type: "SET_FILTER";
|
||||
field: keyof Omit<FilterState, "options" | "debouncedSearchTerm">;
|
||||
value: string;
|
||||
}
|
||||
| { type: "SET_DEBOUNCED_SEARCH"; value: string }
|
||||
| {
|
||||
type: "SET_FILTER_OPTIONS";
|
||||
payload: { categories: string[]; languages: string[] };
|
||||
}
|
||||
| { type: "SET_PAGE"; page: number }
|
||||
| { type: "SET_TOTAL_PAGES"; total: number }
|
||||
| { type: "SET_LOADING"; loading: boolean }
|
||||
| { type: "SET_ERROR"; error: string | null }
|
||||
| { type: "TOGGLE_FILTERS" };
|
||||
|
||||
const initialState: SessionsState = {
|
||||
sessions: [],
|
||||
filters: {
|
||||
searchTerm: "",
|
||||
debouncedSearchTerm: "",
|
||||
category: "",
|
||||
language: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
sortKey: "startTime",
|
||||
sortOrder: "desc",
|
||||
options: { categories: [], languages: [] },
|
||||
},
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 0,
|
||||
pageSize: 10,
|
||||
},
|
||||
ui: {
|
||||
loading: true,
|
||||
error: null,
|
||||
filtersExpanded: false,
|
||||
},
|
||||
};
|
||||
|
||||
function sessionsReducer(
|
||||
state: SessionsState,
|
||||
action: SessionsAction
|
||||
): SessionsState {
|
||||
switch (action.type) {
|
||||
case "SET_SESSIONS":
|
||||
return { ...state, sessions: action.payload };
|
||||
case "SET_FILTER":
|
||||
return {
|
||||
...state,
|
||||
filters: { ...state.filters, [action.field]: action.value },
|
||||
pagination: { ...state.pagination, currentPage: 1 }, // Reset page on filter change
|
||||
};
|
||||
case "SET_DEBOUNCED_SEARCH":
|
||||
return {
|
||||
...state,
|
||||
filters: { ...state.filters, debouncedSearchTerm: action.value },
|
||||
};
|
||||
case "SET_FILTER_OPTIONS":
|
||||
return {
|
||||
...state,
|
||||
filters: { ...state.filters, options: action.payload },
|
||||
};
|
||||
case "SET_PAGE":
|
||||
return {
|
||||
...state,
|
||||
pagination: { ...state.pagination, currentPage: action.page },
|
||||
};
|
||||
case "SET_TOTAL_PAGES":
|
||||
return {
|
||||
...state,
|
||||
pagination: { ...state.pagination, totalPages: action.total },
|
||||
};
|
||||
case "SET_LOADING":
|
||||
return { ...state, ui: { ...state.ui, loading: action.loading } };
|
||||
case "SET_ERROR":
|
||||
return { ...state, ui: { ...state.ui, error: action.error } };
|
||||
case "TOGGLE_FILTERS":
|
||||
return {
|
||||
...state,
|
||||
ui: { ...state.ui, filtersExpanded: !state.ui.filtersExpanded },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SessionsPage() {
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [state, dispatch] = useReducer(sessionsReducer, initialState);
|
||||
const { sessions, filters, pagination, ui } = state;
|
||||
|
||||
const searchHeadingId = useId();
|
||||
const filtersHeadingId = useId();
|
||||
@@ -55,99 +166,66 @@ export default function SessionsPage() {
|
||||
const sortKeyId = useId();
|
||||
const sortKeyHelpId = useId();
|
||||
|
||||
// Filter states
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
categories: [],
|
||||
languages: [],
|
||||
});
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string>("");
|
||||
const [startDate, setStartDate] = useState<string>("");
|
||||
const [endDate, setEndDate] = useState<string>("");
|
||||
|
||||
// Sort states
|
||||
const [sortKey, setSortKey] = useState<string>("startTime"); // Default sort key
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); // Default sort order
|
||||
|
||||
// Debounce search term to avoid excessive API calls
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||
|
||||
// Pagination states
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const [pageSize, _setPageSize] = useState(10); // Or make this configurable
|
||||
|
||||
// UI states
|
||||
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
||||
|
||||
// Debounce search term
|
||||
useEffect(() => {
|
||||
const timerId = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
}, 500); // 500ms delay
|
||||
return () => {
|
||||
clearTimeout(timerId);
|
||||
};
|
||||
}, [searchTerm]);
|
||||
dispatch({ type: "SET_DEBOUNCED_SEARCH", value: filters.searchTerm });
|
||||
}, 500);
|
||||
return () => clearTimeout(timerId);
|
||||
}, [filters.searchTerm]);
|
||||
|
||||
const fetchFilterOptions = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/dashboard/session-filter-options");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch filter options");
|
||||
}
|
||||
if (!response.ok) throw new Error("Failed to fetch filter options");
|
||||
const data = await response.json();
|
||||
setFilterOptions(data);
|
||||
dispatch({ type: "SET_FILTER_OPTIONS", payload: data });
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to load filter options"
|
||||
);
|
||||
dispatch({
|
||||
type: "SET_ERROR",
|
||||
error:
|
||||
err instanceof Error ? err.message : "Failed to load filter options",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchSessions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
dispatch({ type: "SET_LOADING", loading: true });
|
||||
dispatch({ type: "SET_ERROR", error: null });
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (debouncedSearchTerm) params.append("searchTerm", debouncedSearchTerm);
|
||||
if (selectedCategory) params.append("category", selectedCategory);
|
||||
if (selectedLanguage) params.append("language", selectedLanguage);
|
||||
if (startDate) params.append("startDate", startDate);
|
||||
if (endDate) params.append("endDate", endDate);
|
||||
if (sortKey) params.append("sortKey", sortKey);
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
params.append("page", currentPage.toString());
|
||||
params.append("pageSize", pageSize.toString());
|
||||
if (filters.debouncedSearchTerm)
|
||||
params.append("searchTerm", filters.debouncedSearchTerm);
|
||||
if (filters.category) params.append("category", filters.category);
|
||||
if (filters.language) params.append("language", filters.language);
|
||||
if (filters.startDate) params.append("startDate", filters.startDate);
|
||||
if (filters.endDate) params.append("endDate", filters.endDate);
|
||||
if (filters.sortKey) params.append("sortKey", filters.sortKey);
|
||||
if (filters.sortOrder) params.append("sortOrder", filters.sortOrder);
|
||||
params.append("page", pagination.currentPage.toString());
|
||||
params.append("pageSize", pagination.pageSize.toString());
|
||||
|
||||
const response = await fetch(
|
||||
`/api/dashboard/sessions?${params.toString()}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setSessions(data.sessions || []);
|
||||
setTotalPages(Math.ceil((data.totalSessions || 0) / pageSize));
|
||||
dispatch({ type: "SET_SESSIONS", payload: data.sessions || [] });
|
||||
dispatch({
|
||||
type: "SET_TOTAL_PAGES",
|
||||
total: Math.ceil((data.totalSessions || 0) / pagination.pageSize),
|
||||
});
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "An unknown error occurred"
|
||||
);
|
||||
setSessions([]);
|
||||
dispatch({
|
||||
type: "SET_ERROR",
|
||||
error: err instanceof Error ? err.message : "An unknown error occurred",
|
||||
});
|
||||
dispatch({ type: "SET_SESSIONS", payload: [] });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
dispatch({ type: "SET_LOADING", loading: false });
|
||||
}
|
||||
}, [
|
||||
debouncedSearchTerm,
|
||||
selectedCategory,
|
||||
selectedLanguage,
|
||||
startDate,
|
||||
endDate,
|
||||
sortKey,
|
||||
sortOrder,
|
||||
currentPage,
|
||||
pageSize,
|
||||
]);
|
||||
}, [filters, pagination.currentPage, pagination.pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
@@ -167,7 +245,7 @@ export default function SessionsPage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<MessageSquare className="h-6 w-6" />
|
||||
<CardTitle as="h2">Chat Sessions</CardTitle>
|
||||
<CardTitle>Chat Sessions</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -186,8 +264,14 @@ export default function SessionsPage() {
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search sessions (ID, category, initial message...)"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
value={filters.searchTerm}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "searchTerm",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
className="pl-10"
|
||||
aria-label="Search sessions by ID, category, or message content"
|
||||
/>
|
||||
@@ -203,19 +287,19 @@ export default function SessionsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-5 w-5" aria-hidden="true" />
|
||||
<CardTitle as="h2" id={filtersHeadingId} className="text-lg">
|
||||
<CardTitle id={filtersHeadingId} className="text-lg">
|
||||
Filters & Sorting
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setFiltersExpanded(!filtersExpanded)}
|
||||
onClick={() => dispatch({ type: "TOGGLE_FILTERS" })}
|
||||
className="gap-2"
|
||||
aria-expanded={filtersExpanded}
|
||||
aria-expanded={ui.filtersExpanded}
|
||||
aria-controls={filterContentId}
|
||||
>
|
||||
{filtersExpanded ? (
|
||||
{ui.filtersExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
Hide
|
||||
@@ -229,7 +313,7 @@ export default function SessionsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{filtersExpanded && (
|
||||
{ui.filtersExpanded && (
|
||||
<CardContent id={filterContentId}>
|
||||
<fieldset>
|
||||
<legend className="sr-only">
|
||||
@@ -242,12 +326,18 @@ export default function SessionsPage() {
|
||||
<select
|
||||
id={categoryFilterId}
|
||||
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
value={filters.category}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "category",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
aria-describedby={categoryHelpId}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{filterOptions.categories.map((cat) => (
|
||||
{filters.options.categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{formatCategory(cat)}
|
||||
</option>
|
||||
@@ -264,12 +354,18 @@ export default function SessionsPage() {
|
||||
<select
|
||||
id={languageFilterId}
|
||||
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
value={selectedLanguage}
|
||||
onChange={(e) => setSelectedLanguage(e.target.value)}
|
||||
value={filters.language}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "language",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
aria-describedby={languageHelpId}
|
||||
>
|
||||
<option value="">All Languages</option>
|
||||
{filterOptions.languages.map((lang) => (
|
||||
{filters.options.languages.map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{lang.toUpperCase()}
|
||||
</option>
|
||||
@@ -286,8 +382,14 @@ export default function SessionsPage() {
|
||||
<Input
|
||||
type="date"
|
||||
id={startDateFilterId}
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
value={filters.startDate}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "startDate",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
aria-describedby={startDateHelpId}
|
||||
/>
|
||||
<div id={startDateHelpId} className="sr-only">
|
||||
@@ -301,8 +403,14 @@ export default function SessionsPage() {
|
||||
<Input
|
||||
type="date"
|
||||
id={endDateFilterId}
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
value={filters.endDate}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "endDate",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
aria-describedby={endDateHelpId}
|
||||
/>
|
||||
<div id={endDateHelpId} className="sr-only">
|
||||
@@ -316,8 +424,14 @@ export default function SessionsPage() {
|
||||
<select
|
||||
id={sortKeyId}
|
||||
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
value={sortKey}
|
||||
onChange={(e) => setSortKey(e.target.value)}
|
||||
value={filters.sortKey}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "sortKey",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
aria-describedby={sortKeyHelpId}
|
||||
>
|
||||
<option value="startTime">Start Time</option>
|
||||
@@ -340,9 +454,13 @@ export default function SessionsPage() {
|
||||
<select
|
||||
id={sortOrderId}
|
||||
className="w-full h-10 px-3 py-2 text-sm rounded-md border border-input bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
value={sortOrder}
|
||||
value={filters.sortOrder}
|
||||
onChange={(e) =>
|
||||
setSortOrder(e.target.value as "asc" | "desc")
|
||||
dispatch({
|
||||
type: "SET_FILTER",
|
||||
field: "sortOrder",
|
||||
value: e.target.value as "asc" | "desc",
|
||||
})
|
||||
}
|
||||
aria-describedby={sortOrderHelpId}
|
||||
>
|
||||
@@ -368,17 +486,20 @@ export default function SessionsPage() {
|
||||
|
||||
{/* Live region for screen reader announcements */}
|
||||
<output aria-live="polite" className="sr-only">
|
||||
{loading && "Loading sessions..."}
|
||||
{error && `Error loading sessions: ${error}`}
|
||||
{!loading &&
|
||||
!error &&
|
||||
{ui.loading && "Loading sessions..."}
|
||||
{ui.error && `Error loading sessions: ${ui.error}`}
|
||||
{!ui.loading &&
|
||||
!ui.error &&
|
||||
sessions.length > 0 &&
|
||||
`Found ${sessions.length} sessions`}
|
||||
{!loading && !error && sessions.length === 0 && "No sessions found"}
|
||||
{!ui.loading &&
|
||||
!ui.error &&
|
||||
sessions.length === 0 &&
|
||||
"No sessions found"}
|
||||
</output>
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
{ui.loading && (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div
|
||||
@@ -392,7 +513,7 @@ export default function SessionsPage() {
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
{ui.error && (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div
|
||||
@@ -400,19 +521,19 @@ export default function SessionsPage() {
|
||||
role="alert"
|
||||
aria-hidden="true"
|
||||
>
|
||||
Error: {error}
|
||||
Error: {ui.error}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && !error && sessions.length === 0 && (
|
||||
{!ui.loading && !ui.error && sessions.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{debouncedSearchTerm
|
||||
? `No sessions found for "${debouncedSearchTerm}".`
|
||||
{filters.debouncedSearchTerm
|
||||
? `No sessions found for "${filters.debouncedSearchTerm}".`
|
||||
: "No sessions found."}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -420,7 +541,7 @@ export default function SessionsPage() {
|
||||
)}
|
||||
|
||||
{/* Sessions List */}
|
||||
{!loading && !error && sessions.length > 0 && (
|
||||
{!ui.loading && !ui.error && sessions.length > 0 && (
|
||||
<ul aria-label="Chat sessions" className="grid gap-4">
|
||||
{sessions.map((session) => (
|
||||
<li key={session.id}>
|
||||
@@ -508,30 +629,39 @@ export default function SessionsPage() {
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 0 && (
|
||||
{pagination.totalPages > 0 && (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex justify-center items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.max(prev - 1, 1))
|
||||
dispatch({
|
||||
type: "SET_PAGE",
|
||||
page: Math.max(pagination.currentPage - 1, 1),
|
||||
})
|
||||
}
|
||||
disabled={currentPage === 1}
|
||||
disabled={pagination.currentPage === 1}
|
||||
className="gap-2"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {currentPage} of {totalPages}
|
||||
Page {pagination.currentPage} of {pagination.totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
dispatch({
|
||||
type: "SET_PAGE",
|
||||
page: Math.min(
|
||||
pagination.currentPage + 1,
|
||||
pagination.totalPages
|
||||
),
|
||||
})
|
||||
}
|
||||
disabled={currentPage === totalPages}
|
||||
disabled={pagination.currentPage === pagination.totalPages}
|
||||
className="gap-2"
|
||||
>
|
||||
Next
|
||||
|
||||
Reference in New Issue
Block a user