- Refactored AdministratorController, LecturerController, and StudentController to utilize Inertia's scroll feature for paginated data. - Enhanced DataCards and DataTable components to support infinite scrolling, allowing for a smoother user experience when navigating large datasets. - Updated multiple admin pages (e.g., MaterialIndex, LogIndex, FeedbackIndex) to integrate infinite scroll functionality, improving data loading efficiency. - Adjusted server table hooks to reset keys appropriately for infinite scroll scenarios.
229 lines
8.7 KiB
TypeScript
229 lines
8.7 KiB
TypeScript
import { InfiniteScroll } from '@inertiajs/react';
|
|
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ChevronsLeft,
|
|
ChevronsRight,
|
|
} from 'lucide-react';
|
|
import * as React from 'react';
|
|
import type { PaginationState } from '@/components/data-table';
|
|
import { useDebounce } from '@/components/data-table';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
|
|
export type { PaginationState } from '@/components/data-table';
|
|
|
|
interface DataCardsProps<TData> {
|
|
data: TData[];
|
|
renderCard: (item: TData, index: number) => React.ReactNode;
|
|
getRowId?: (item: TData) => string | number;
|
|
searchKey?: string;
|
|
searchPlaceholder?: string;
|
|
emptyText?: string;
|
|
toolbar?: React.ReactNode;
|
|
pagination: PaginationState;
|
|
onPageChange?: (page: number) => void;
|
|
onPerPageChange?: (perPage: number) => void;
|
|
onSearchChange?: (search: string) => void;
|
|
searchValue?: string;
|
|
/**
|
|
* Enables infinite scroll instead of page-based pagination. `propName`
|
|
* must match the Inertia page prop returned via `Inertia::scroll()` on
|
|
* the backend so the frontend can track its merge/scroll metadata.
|
|
*/
|
|
infiniteScroll?: {
|
|
propName: string;
|
|
buffer?: number;
|
|
};
|
|
}
|
|
|
|
export function DataCards<TData>({
|
|
data,
|
|
renderCard,
|
|
getRowId,
|
|
searchKey,
|
|
searchPlaceholder = 'Ketikkan sesuatu...',
|
|
emptyText = 'Tidak ada data.',
|
|
toolbar,
|
|
pagination,
|
|
onPageChange,
|
|
onPerPageChange,
|
|
onSearchChange,
|
|
searchValue,
|
|
infiniteScroll,
|
|
}: DataCardsProps<TData>) {
|
|
const isInfiniteScroll = !!infiniteScroll;
|
|
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
|
const [previousSearchValue, setPreviousSearchValue] =
|
|
React.useState(searchValue);
|
|
|
|
if (searchValue !== previousSearchValue) {
|
|
setPreviousSearchValue(searchValue);
|
|
setLocalSearch(searchValue ?? '');
|
|
}
|
|
|
|
const handleSearchDebounced = useDebounce(
|
|
(value: string) => onSearchChange?.(value),
|
|
300,
|
|
);
|
|
|
|
function handleSearchChange(value: string) {
|
|
setLocalSearch(value);
|
|
handleSearchDebounced(value);
|
|
}
|
|
|
|
const currentPage = pagination.current_page;
|
|
const totalPages = pagination.last_page;
|
|
|
|
return (
|
|
<>
|
|
{(searchKey || toolbar || onPerPageChange) && (
|
|
<div className="flex items-center gap-2">
|
|
{searchKey && (
|
|
<Input
|
|
placeholder={searchPlaceholder}
|
|
value={localSearch}
|
|
onChange={(event) =>
|
|
handleSearchChange(event.target.value)
|
|
}
|
|
className="max-w-sm"
|
|
/>
|
|
)}
|
|
{toolbar}
|
|
<div className="ml-auto flex items-center gap-2">
|
|
{onPerPageChange && (
|
|
<Select
|
|
value={String(pagination.per_page)}
|
|
onValueChange={(value) =>
|
|
onPerPageChange(Number(value))
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 w-[70px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="25">25</SelectItem>
|
|
<SelectItem value="50">50</SelectItem>
|
|
<SelectItem value="100">100</SelectItem>
|
|
<SelectItem value="999999">
|
|
Semua
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{data.length ? (
|
|
isInfiniteScroll ? (
|
|
<InfiniteScroll
|
|
data={infiniteScroll.propName}
|
|
as="div"
|
|
onlyNext
|
|
preserveUrl
|
|
buffer={infiniteScroll.buffer ?? 300}
|
|
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
|
next={({ loading, hasMore }) =>
|
|
loading ? (
|
|
<p className="py-2 text-center text-sm text-muted-foreground">
|
|
Memuat data...
|
|
</p>
|
|
) : hasMore ? null : (
|
|
<p className="py-2 text-center text-sm text-muted-foreground">
|
|
Semua data telah ditampilkan.
|
|
</p>
|
|
)
|
|
}
|
|
>
|
|
{data.map((item, index) => (
|
|
<Card
|
|
key={getRowId ? getRowId(item) : index}
|
|
className="overflow-hidden"
|
|
>
|
|
<CardContent>
|
|
{renderCard(item, index)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</InfiniteScroll>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{data.map((item, index) => (
|
|
<Card
|
|
key={getRowId ? getRowId(item) : index}
|
|
className="overflow-hidden"
|
|
>
|
|
<CardContent>
|
|
{renderCard(item, index)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)
|
|
) : (
|
|
<Card>
|
|
<CardContent className="flex h-24 items-center justify-center text-center text-muted-foreground">
|
|
{emptyText}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{isInfiniteScroll ? (
|
|
<p className="text-center text-sm text-muted-foreground">
|
|
Menampilkan {data.length.toLocaleString('id-ID')} dari{' '}
|
|
{pagination.total.toLocaleString('id-ID')} data
|
|
</p>
|
|
) : (
|
|
<div className="flex items-center justify-end gap-2">
|
|
<span className="text-sm text-muted-foreground">
|
|
{`Halaman ${currentPage} dari ${totalPages}`}
|
|
</span>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange?.(1)}
|
|
disabled={currentPage <= 1}
|
|
>
|
|
<ChevronsLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange?.(currentPage - 1)}
|
|
disabled={currentPage <= 1}
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange?.(currentPage + 1)}
|
|
disabled={currentPage >= totalPages}
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange?.(totalPages)}
|
|
disabled={currentPage >= totalPages}
|
|
>
|
|
<ChevronsRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|