metatron-open-seo/src/client/features/ai-search/components/BrandLookupCitationTables.tsx
Ben Senescu 9e5afc9d48
feat: Add shared table component (#154)
* fix rank tracking unranked filters

* improve modal dismissal and range selection

* refactor: create shared table component
2026-05-06 11:23:07 -04:00

170 lines
5.0 KiB
TypeScript

import { createColumnHelper, type Table } from "@tanstack/react-table";
import { ExternalLink } from "lucide-react";
import { AppDataTable } from "@/client/components/table/AppDataTable";
import { SortableHeader } from "@/client/components/table/SortableHeader";
import { numericNullsLast } from "@/client/components/table/nullSafeSort";
import {
formatCount,
formatPlatformLabel,
} from "@/client/features/ai-search/platformLabels";
import { formatUrlForDisplay } from "@/client/components/table/url";
import type { BrandLookupResult } from "@/types/schemas/ai-search";
type TopPageRow = BrandLookupResult["topPages"][number];
type TopQueryRow = BrandLookupResult["topQueries"][number];
type PlatformKey = TopPageRow["platform"];
const PLATFORM_BADGE_CLASS: Record<PlatformKey, string> = {
chat_gpt: "border-emerald-500/40 bg-emerald-500/10 text-emerald-500",
google: "border-sky-500/40 bg-sky-500/10 text-sky-500",
};
function PlatformBadge({ platform }: { platform: PlatformKey }) {
return (
<span className={`badge badge-sm border ${PLATFORM_BADGE_CLASS[platform]}`}>
{formatPlatformLabel(platform)}
</span>
);
}
const pagesHelper = createColumnHelper<TopPageRow>();
const queriesHelper = createColumnHelper<TopQueryRow>();
export const topPagesColumns = [
pagesHelper.accessor("url", {
id: "url",
header: () => <span className="uppercase tracking-wider">URL</span>,
enableSorting: false,
cell: ({ row }) => (
<>
<a
href={row.original.url}
target="_blank"
rel="noreferrer"
className="link link-primary inline-flex items-start gap-1 break-all"
>
<span className="break-all">
{formatUrlForDisplay(row.original.url)}
</span>
<ExternalLink className="mt-1 size-3 shrink-0" />
</a>
{row.original.domain ? (
<p className="text-xs text-base-content/50">{row.original.domain}</p>
) : null}
</>
),
}),
pagesHelper.accessor("platform", {
id: "platform",
header: () => <span className="uppercase tracking-wider">Platform</span>,
enableSorting: false,
cell: ({ getValue }) => <PlatformBadge platform={getValue()} />,
}),
pagesHelper.accessor("mentions", {
id: "mentions",
header: ({ column }) => (
<SortableHeader column={column} label="Mentions" align="right" />
),
cell: ({ getValue }) => (
<span className="tabular-nums">{formatCount(getValue())}</span>
),
sortingFn: numericNullsLast,
sortDescFirst: true,
}),
];
export const topQueriesColumns = [
queriesHelper.accessor("question", {
id: "question",
header: () => <span className="uppercase tracking-wider">Query</span>,
enableSorting: false,
cell: ({ row }) => (
<>
<p className="break-words font-medium">{row.original.question}</p>
{row.original.brandsMentioned.length > 0 ? (
<p className="mt-0.5 text-xs text-base-content/50">
Brands: {row.original.brandsMentioned.slice(0, 5).join(", ")}
</p>
) : null}
</>
),
}),
queriesHelper.accessor("platform", {
id: "platform",
header: () => <span className="uppercase tracking-wider">Platform</span>,
enableSorting: false,
cell: ({ getValue }) => <PlatformBadge platform={getValue()} />,
}),
queriesHelper.accessor("aiSearchVolume", {
id: "aiSearchVolume",
header: ({ column }) => (
<SortableHeader column={column} label="AI search vol." align="right" />
),
cell: ({ getValue }) => (
<span className="tabular-nums">{formatCount(getValue())}</span>
),
sortingFn: numericNullsLast,
sortDescFirst: true,
}),
];
export function TopPagesTable({ table }: { table: Table<TopPageRow> }) {
if (table.getRowModel().rows.length === 0) {
return (
<p className="p-6 text-center text-sm text-base-content/60">
No cited pages returned.
</p>
);
}
return <BrandLookupTable table={table} urlLikeColumnId="url" />;
}
export function TopQueriesTable({ table }: { table: Table<TopQueryRow> }) {
if (table.getRowModel().rows.length === 0) {
return (
<p className="p-6 text-center text-sm text-base-content/60">
No matching queries found.
</p>
);
}
return <BrandLookupTable table={table} urlLikeColumnId="question" />;
}
function BrandLookupTable<T>({
table,
urlLikeColumnId,
}: {
table: Table<T>;
urlLikeColumnId: string;
}) {
return (
<AppDataTable
table={table}
getCellClassName={(_, columnId) =>
cellClassName(
columnId,
urlLikeColumnId,
table.getColumn(columnId)?.getCanSort() ?? false,
)
}
getRowClassName={() => ""}
/>
);
}
function cellClassName(
columnId: string,
urlLikeColumnId: string,
isNumeric: boolean,
): string {
if (columnId === urlLikeColumnId) {
return "min-w-80 max-w-2xl align-top";
}
if (isNumeric) {
return "whitespace-nowrap text-right align-top";
}
return "whitespace-nowrap align-top";
}