• Renders a paginated table of manga items with load-more functionality. Displays columns based on data presence: title, status, chapters, volumes, score, last read.

    Parameters

    Returns Element

    Paginated table component with load more button.

    export function DataTable({
    data,
    itemsPerPage = 50,
    isLoading = false,
    }: Readonly<DataTableProps>) {
    const [visibleData, setVisibleData] = useState<KenmeiMangaItem[]>([]);
    const [displayCount, setDisplayCount] = useState(itemsPerPage);
    const [isLoadingMore, setIsLoadingMore] = useState(false);
    const scrollAreaRef = useRef<HTMLDivElement>(null);
    const tablePoolRef = useRef(getDataTableWorkerPool());

    // Determine which optional columns to display based on data presence
    const hasScore = data.some(
    (item) => item.score !== undefined && item.score > 0,
    );
    const hasChapters = data.some(
    (item) => item.chaptersRead !== undefined && item.chaptersRead > 0,
    );
    const hasVolumes = data.some(
    (item) => item.volumesRead !== undefined && item.volumesRead > 0,
    );
    const hasLastRead = data.some((item) => item.updatedAt || item.createdAt);

    const columnVisibility = useMemo(
    () => ({
    score: hasScore,
    chapters: hasChapters,
    volumes: hasVolumes,
    lastRead: hasLastRead,
    }),
    [hasScore, hasChapters, hasVolumes, hasLastRead],
    );

    useEffect(() => {
    setVisibleData(data.slice(0, displayCount));
    }, [data, displayCount]);

    useEffect(() => {
    setDisplayCount(itemsPerPage);
    setVisibleData(data.slice(0, itemsPerPage));
    }, [data, itemsPerPage]);

    /**
    * Loads additional manga items
    * Scrolls to the bottom of the table after loading completes.
    * @source
    */
    const handleLoadMore = async () => {
    if (displayCount >= data.length) {
    return;
    }

    setIsLoadingMore(true);

    try {
    const startIndex = displayCount;
    const endIndex = Math.min(displayCount + itemsPerPage, data.length);

    // Use worker pool to preprocess the new data slice
    // This offloads formatting and row height computation to a worker thread
    const prepResult = await tablePoolRef.current.prepareTableSlice(
    data,
    startIndex,
    endIndex,
    itemsPerPage,
    columnVisibility,
    );

    // Log worker performance metrics
    console.debug(
    `[DataTable] Loaded ${prepResult.preparedData.length} rows in ${prepResult.timing.totalTimeMs.toFixed(2)}ms (worker: ${prepResult.ranOnWorker ? "yes" : "no"})`,
    );

    // Update display count and visible data
    setDisplayCount((prev) => Math.min(prev + itemsPerPage, data.length));
    setIsLoadingMore(false);

    // Scroll to bottom after new items are loaded
    if (scrollAreaRef.current) {
    const scrollViewport = scrollAreaRef.current.querySelector(
    "[data-radix-scroll-area-viewport]",
    );
    if (scrollViewport) {
    setTimeout(() => {
    scrollViewport.scrollTop = scrollViewport.scrollHeight;
    }, 0);
    }
    }
    } catch (error) {
    console.error("[DataTable] Failed to load more data:", error);
    setIsLoadingMore(false);
    }
    };

    /**
    * Formats an ISO date string into a localized date representation.
    * @param dateString - ISO date string or undefined.
    * @returns Localized date string or "-" if invalid/undefined.
    * @source
    */
    const formatDate = (dateString: string | undefined) => {
    if (!dateString) return "-";
    try {
    const date = new Date(dateString);
    return date.toLocaleDateString();
    } catch {
    return "-";
    }
    };

    /**
    * Returns a styled Badge component for the given manga status.
    * @param status - Manga status string.
    * @returns Badge component with status-specific styling and label.
    * @source
    */
    const getStatusBadge = (status: string) => {
    switch (status) {
    case "reading":
    return (
    <Badge
    variant="outline"
    className="border-green-200 bg-green-100 text-green-800 hover:bg-green-100 dark:border-green-800 dark:bg-green-900/30 dark:text-green-400"
    >
    {status.replaceAll("_", " ")}
    </Badge>
    );
    case "completed":
    return (
    <Badge
    variant="outline"
    className="border-blue-200 bg-blue-100 text-blue-800 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
    >
    {status.replaceAll("_", " ")}
    </Badge>
    );
    case "on_hold":
    return (
    <Badge
    variant="outline"
    className="border-amber-200 bg-amber-100 text-amber-800 hover:bg-amber-100 dark:border-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
    >
    {status.replaceAll("_", " ")}
    </Badge>
    );
    case "dropped":
    return (
    <Badge
    variant="outline"
    className="border-red-200 bg-red-100 text-red-800 hover:bg-red-100 dark:border-red-800 dark:bg-red-900/30 dark:text-red-400"
    >
    {status.replaceAll("_", " ")}
    </Badge>
    );
    case "plan_to_read":
    return (
    <Badge
    variant="outline"
    className="border-purple-200 bg-purple-100 text-purple-800 hover:bg-purple-100 dark:border-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
    >
    {status.replaceAll("_", " ")}
    </Badge>
    );
    default:
    return (
    <Badge
    variant="outline"
    className="border-gray-200 bg-gray-100 text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900/30 dark:text-gray-400"
    >
    {status.replaceAll("_", " ")}
    </Badge>
    );
    }
    };

    return (
    <div className="rounded-md border" aria-busy={isLoading}>
    <ScrollArea ref={scrollAreaRef} className="h-[500px] rounded-md">
    <Table>
    <TableCaption>
    Showing {visibleData.length} of {data.length} entries
    </TableCaption>

    <TableHeader className="bg-muted/50 sticky top-0 backdrop-blur-sm">
    <TableRow>
    <TableHead className="w-[45%] min-w-[200px]">Title</TableHead>
    <TableHead>Status</TableHead>

    {hasChapters && <TableHead className="w-20">Ch</TableHead>}
    {hasVolumes && <TableHead className="w-20">Vol</TableHead>}
    {hasScore && <TableHead className="w-20">Score</TableHead>}

    {hasLastRead && (
    <TableHead className="w-[120px]">Last Read</TableHead>
    )}
    </TableRow>
    </TableHeader>

    <TableBody>
    {isLoading || isLoadingMore
    ? // Render skeleton rows during loading, regardless of data presence
    Array.from({ length: 10 }).map((_, index) => (
    <TableRow key={`skeleton-row-${index + 1}`}>
    <TableCell className="max-w-[300px] truncate">
    <Skeleton className="h-4 w-full" />
    </TableCell>
    <TableCell>
    <Skeleton className="h-6 w-16" />
    </TableCell>
    {hasChapters && (
    <TableCell>
    <Skeleton className="h-4 w-8" />
    </TableCell>
    )}
    {hasVolumes && (
    <TableCell>
    <Skeleton className="h-4 w-8" />
    </TableCell>
    )}
    {hasScore && (
    <TableCell>
    <Skeleton className="h-4 w-12" />
    </TableCell>
    )}
    {hasLastRead && (
    <TableCell>
    <Skeleton className="h-4 w-20" />
    </TableCell>
    )}
    </TableRow>
    ))
    : visibleData.map((item) => (
    <TableRow
    key={`${item.title}-${item.status}-${item.updatedAt ?? item.createdAt}`}
    className="hover:bg-muted/40"
    >
    <TableCell
    className="max-w-[300px] truncate font-medium"
    title={item.title}
    >
    {item.title}
    </TableCell>

    <TableCell>{getStatusBadge(item.status)}</TableCell>

    {hasChapters && (
    <TableCell className="text-muted-foreground">
    {item.chaptersRead || "-"}
    </TableCell>
    )}

    {hasVolumes && (
    <TableCell className="text-muted-foreground">
    {item.volumesRead || "-"}
    </TableCell>
    )}

    {hasScore && (
    <TableCell className="text-muted-foreground">
    {item.score ? item.score.toFixed(1) : "-"}
    </TableCell>
    )}

    {hasLastRead && (
    <TableCell
    className="text-muted-foreground"
    title={item.updatedAt || item.createdAt}
    >
    {formatDate(item.updatedAt || item.createdAt)}
    </TableCell>
    )}
    </TableRow>
    ))}

    {/* Display empty state when no items are visible and not loading */}
    {visibleData.length === 0 && !isLoading && (
    <TableRow>
    <TableCell
    colSpan={
    2 +
    (hasChapters ? 1 : 0) +
    (hasVolumes ? 1 : 0) +
    (hasScore ? 1 : 0) +
    (hasLastRead ? 1 : 0)
    }
    className="h-24 text-center"
    >
    No manga entries found
    </TableCell>
    </TableRow>
    )}
    </TableBody>
    </Table>
    </ScrollArea>

    {/* Load more button */}
    {visibleData.length < data.length && (
    <div className="flex justify-center border-t p-4">
    <Button
    variant="outline"
    onClick={handleLoadMore}
    disabled={isLoading || isLoadingMore}
    aria-disabled={isLoading || isLoadingMore}
    className="w-full max-w-xs"
    >
    {isLoadingMore ? (
    <>
    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
    Loading...
    </>
    ) : (
    <>
    <ChevronDown className="mr-2 h-4 w-4" />
    Load More ({data.length - visibleData.length} remaining)
    </>
    )}
    </Button>
    </div>
    )}
    </div>
    );
    }