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:
2026-01-20 07:28:10 +01:00
parent 8b3846539f
commit 5bfd762e55
161 changed files with 14655 additions and 11682 deletions

View File

@@ -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