← back to Open Seo
src/client/features/domain/components/DomainPagesTable.tsx
110 lines
import { memo, useMemo } from "react";
import { createColumnHelper, type ColumnDef } from "@tanstack/react-table";
import {
AppDataTable,
useAppTable,
} from "@/client/components/table/AppDataTable";
import { ExternalUrlCell } from "@/client/components/table/url";
import { SortableHeader } from "@/client/features/domain/components/SortableHeader";
import { useDomainRenderDebug } from "@/client/features/domain/domainDebug";
import {
formatNumber,
formatRounded,
toPageSortMode,
} from "@/client/features/domain/utils";
import type {
DomainSortMode,
PageRow,
SortOrder,
} from "@/client/features/domain/types";
type Props = {
domain: string;
rows: PageRow[];
sortMode: DomainSortMode;
currentSortOrder: SortOrder;
onSortClick: (sort: DomainSortMode) => void;
};
const pageColumnHelper = createColumnHelper<PageRow>();
function DomainPagesTableComponent({
domain,
rows,
sortMode,
currentSortOrder,
onSortClick,
}: Props) {
const renderStarted = performance.now();
const columns = useMemo<ColumnDef<PageRow>[]>(
() => [
pageColumnHelper.display({
id: "page",
header: () => "Page",
cell: ({ row }) => (
<ExternalUrlCell
value={row.original.relativePath ?? row.original.page}
label={row.original.relativePath ?? row.original.page}
baseDomain={domain}
className="link link-primary inline-flex items-center gap-1"
/>
),
meta: {
cellClassName: "max-w-[420px] truncate",
},
}),
pageColumnHelper.accessor("organicTraffic", {
header: () => (
<SortableHeader
label="Organic Traffic"
isActive={toPageSortMode(sortMode) === "traffic"}
order={currentSortOrder}
onClick={() => onSortClick("traffic")}
/>
),
cell: ({ getValue }) => formatRounded(getValue()),
}),
pageColumnHelper.accessor("keywords", {
header: () => (
<SortableHeader
label="Keywords"
isActive={toPageSortMode(sortMode) === "keywords"}
order={currentSortOrder}
onClick={() => onSortClick("volume")}
/>
),
cell: ({ getValue }) => formatNumber(getValue()),
}),
],
[currentSortOrder, domain, onSortClick, sortMode],
);
// Memoized on purpose: a fresh slice every render defeats TanStack Table's
// data-keyed memo, so _autoResetPageIndex fires each render and its setState
// schedules another one — an unbounded render loop that freezes the tab.
const tableData = useMemo(() => rows.slice(0, 100), [rows]);
const table = useAppTable({
data: tableData,
columns,
});
useDomainRenderDebug("DomainPagesTable", {
rows: rows.length,
durationMs: Math.round(performance.now() - renderStarted),
sortMode,
currentSortOrder,
});
return (
<AppDataTable
table={table}
className="table table-sm"
empty={
<div className="py-6 text-center text-base-content/60">
No pages match this search.
</div>
}
/>
);
}
export const DomainPagesTable = memo(DomainPagesTableComponent);