metatron-open-seo/src/client/features/backlinks/BacklinksTableHeaders.tsx
Ben Senescu 8405564925
feat: Add Backlinks page (#23)
* feat: add backlinks setup and overview workflow

* save

* fix backlinks timeseries and refine overview layout

* update backlinks table guidance

* refactor backlinks CI cleanup

* refactor backlinks page state colocation

* fix backlinks access error handling

Differentiate DataForSEO subscription access failures from billing issues and preserve exact page hosts for page-level backlink lookups.

* fix backlinks project scoping and error states

* simplify backlinks billing and lazy loading

* harden backlinks profiling and access flow

* refine backlinks access status typing

* update backlinks scope selection and access handling

* fix backlinks scope fallback and cache scoping

* fix backlinks ci checks

* docs: simplify backlinks pricing

* refine backlinks search: move scope toggle below input, remove redundant description

* refine backlinks summaries and help text

* refine backlinks table sorting

* fix backlinks invalid target handling

* delete docs

* fix backlinks scope inference and domain normalization

* docs: mark backlinks as a supported workflow

* fix backlinks links badge wrapping

* test: split backlinks test suites
2026-03-16 17:48:13 -04:00

255 lines
6.8 KiB
TypeScript

import { ArrowDown, ArrowUp } from "lucide-react";
import { HeaderHelpLabel } from "@/client/features/keywords/components";
import {
getNextSort,
type BacklinksTableSort,
type ReferringDomainsTableSort,
type SortDirection,
type TopPagesTableSort,
} from "./backlinksTableSorting";
export function BacklinksTableHeader({
sort,
onSortChange,
}: {
sort: BacklinksTableSort;
onSortChange: (sort: BacklinksTableSort) => void;
}) {
return (
<thead>
<tr>
<SortableHeaderCell
label="Source"
helpText="Page or domain linking to you"
field="source"
defaultDirection="asc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Target"
helpText="Destination on your site"
field="target"
defaultDirection="asc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Anchor"
helpText="Text or format of the link"
field="anchor"
defaultDirection="asc"
sort={sort}
onSortChange={onSortChange}
/>
<th>
<HeaderHelpLabel
label="Flags"
helpText="Special backlink attributes, such as lost, broken, nofollow, or multiple links from the same source."
/>
</th>
<SortableHeaderCell
align="right"
label="Link"
helpText="Authority of the linking page"
field="linkAuthority"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
align="right"
label="DA"
helpText="Authority of the linking domain"
field="domainAuthority"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="First Seen"
helpText="When this link was first discovered by the crawler"
field="firstSeen"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
</tr>
</thead>
);
}
export function ReferringDomainsTableHeader({
sort,
onSortChange,
}: {
sort: ReferringDomainsTableSort;
onSortChange: (sort: ReferringDomainsTableSort) => void;
}) {
return (
<thead>
<tr>
<SortableHeaderCell
label="Domain"
helpText="The referring site linking to your target."
field="domain"
defaultDirection="asc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Backlinks"
helpText="Total backlinks found from this domain."
field="backlinks"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Referring Pages"
helpText="Unique pages on this domain that link to your target."
field="referringPages"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Rank"
helpText="Authority score for the referring domain."
field="rank"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Spam"
helpText="Spam risk score for this referring domain."
field="spamScore"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="First Seen"
helpText="When this domain was first discovered linking to your target."
field="firstSeen"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Issues"
helpText="Broken link and broken page counts tied to this domain."
field="issues"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
</tr>
</thead>
);
}
export function TopPagesTableHeader({
sort,
onSortChange,
}: {
sort: TopPagesTableSort;
onSortChange: (sort: TopPagesTableSort) => void;
}) {
return (
<thead>
<tr>
<SortableHeaderCell
label="Page"
helpText="Page on the target site receiving backlinks."
field="page"
defaultDirection="asc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Backlinks"
helpText="Total backlinks pointing to this page."
field="backlinks"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Referring Domains"
helpText="Unique domains linking to this page."
field="referringDomains"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Rank"
helpText="Authority score for this target page."
field="rank"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
<SortableHeaderCell
label="Broken Backlinks"
helpText="Backlinks pointing here that are currently broken."
field="brokenBacklinks"
defaultDirection="desc"
sort={sort}
onSortChange={onSortChange}
/>
</tr>
</thead>
);
}
function SortableHeaderCell<TField extends string>({
align,
label,
helpText,
field,
defaultDirection,
sort,
onSortChange,
}: {
align?: "left" | "right";
label: string;
helpText: string;
field: TField;
defaultDirection: SortDirection;
sort: { field: TField; direction: SortDirection };
onSortChange: (sort: { field: TField; direction: SortDirection }) => void;
}) {
const isActive = sort.field === field;
const content = (
<button
type="button"
className="inline-flex items-center gap-1 font-medium transition-colors hover:text-base-content"
onClick={() => onSortChange(getNextSort(sort, field, defaultDirection))}
aria-label={`Sort by ${label}`}
aria-pressed={isActive}
>
<HeaderHelpLabel label={label} helpText={helpText} />
{isActive ? (
sort.direction === "asc" ? (
<ArrowUp className="size-3 shrink-0" />
) : (
<ArrowDown className="size-3 shrink-0" />
)
) : null}
</button>
);
return (
<th className={align === "right" ? "text-right" : undefined}>
{align === "right" ? (
<span className="inline-flex justify-end">{content}</span>
) : (
content
)}
</th>
);
}