import { createColumnHelper, type SortingFn, type SortingState, } from "@tanstack/react-table"; import { useState } from "react"; import { AppDataTable, useAppTable, } from "@/client/components/table/AppDataTable"; import { SortableHeader } from "@/client/components/table/SortableHeader"; import { compareNumericNullsLast, dateNullsLast, isDescending, numericNullsLast, stringNullsLast, } from "@/client/components/table/nullSafeSort"; import { EmptyTableState } from "./BacklinksPageEmptyTableState"; import type { BacklinksOverviewData } from "./backlinksPageTypes"; import { formatCompactDate, formatDecimal, formatNumber, } from "./backlinksPageUtils"; import { BacklinksExternalLink } from "./BacklinksPageLinks"; type ReferringDomainRow = BacklinksOverviewData["referringDomains"][number]; const columnHelper = createColumnHelper(); // Nulls always to the bottom in both directions, same as the pre-TanStack // implementation. Secondary compare on brokenPages must also keep nulls last — // coercing to 0 would mix unknown values with real zeroes. const sortByIssues: SortingFn = (left, right, columnId) => { const descending = isDescending(left, columnId); const primary = compareNumericNullsLast( left.original.brokenBacklinks, right.original.brokenBacklinks, descending, ); if (primary !== 0) return primary; return compareNumericNullsLast( left.original.brokenPages, right.original.brokenPages, descending, ); }; const columns = [ columnHelper.accessor("domain", { header: ({ column }) => ( ), cell: ({ getValue }) => { const domain = getValue(); if (!domain) return "-"; return ( ); }, sortingFn: stringNullsLast, }), columnHelper.accessor("backlinks", { header: ({ column }) => ( ), cell: ({ getValue }) => formatNumber(getValue()), sortingFn: numericNullsLast, sortDescFirst: true, }), columnHelper.accessor("referringPages", { header: ({ column }) => ( ), cell: ({ getValue }) => formatNumber(getValue()), sortingFn: numericNullsLast, sortDescFirst: true, }), columnHelper.accessor("rank", { header: ({ column }) => ( ), cell: ({ getValue }) => formatNumber(getValue()), sortingFn: numericNullsLast, sortDescFirst: true, }), columnHelper.accessor("spamScore", { header: ({ column }) => ( ), cell: ({ getValue }) => formatDecimal(getValue()), sortingFn: numericNullsLast, sortDescFirst: true, }), columnHelper.accessor("firstSeen", { header: ({ column }) => ( ), cell: ({ getValue }) => formatCompactDate(getValue()), sortingFn: dateNullsLast, sortDescFirst: true, }), columnHelper.display({ id: "issues", header: ({ column }) => ( ), cell: ({ row }) => (
Broken links: {formatNumber(row.original.brokenBacklinks)}
Broken pages: {formatNumber(row.original.brokenPages)}
), enableSorting: true, sortingFn: sortByIssues, sortDescFirst: true, }), ]; const DEFAULT_SORTING: SortingState = [{ id: "backlinks", desc: true }]; function getDomainWebsiteHref(domain: string) { try { return new URL(domain).toString(); } catch { return `https://${domain}`; } } export function ReferringDomainsTable({ rows, }: { rows: BacklinksOverviewData["referringDomains"]; }) { const [sorting, setSorting] = useState(DEFAULT_SORTING); const table = useAppTable({ data: rows, columns, state: { sorting }, onSortingChange: setSorting, withSorting: true, }); if (rows.length === 0) { return ; } return ( columnId === "domain" ? "font-medium break-all" : undefined } /> ); }