metatron-open-seo/src/client/features/backlinks/backlinksTableSorting.ts
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

218 lines
5.7 KiB
TypeScript

import type { BacklinksOverviewData } from "./backlinksPageTypes";
export type SortDirection = "asc" | "desc";
export type BacklinksTableSortField =
| "source"
| "target"
| "anchor"
| "linkAuthority"
| "domainAuthority"
| "firstSeen";
export type ReferringDomainsTableSortField =
| "domain"
| "backlinks"
| "referringPages"
| "rank"
| "spamScore"
| "firstSeen"
| "issues";
export type TopPagesTableSortField =
| "page"
| "backlinks"
| "referringDomains"
| "rank"
| "brokenBacklinks";
export type TableSort<TField extends string> = {
field: TField;
direction: SortDirection;
};
export type BacklinksTableSort = TableSort<BacklinksTableSortField>;
export type ReferringDomainsTableSort =
TableSort<ReferringDomainsTableSortField>;
export type TopPagesTableSort = TableSort<TopPagesTableSortField>;
export const DEFAULT_BACKLINKS_SORT: BacklinksTableSort = {
field: "firstSeen",
direction: "desc",
};
export const DEFAULT_REFERRING_DOMAINS_SORT: ReferringDomainsTableSort = {
field: "backlinks",
direction: "desc",
};
export const DEFAULT_TOP_PAGES_SORT: TopPagesTableSort = {
field: "backlinks",
direction: "desc",
};
export function getNextSort<TField extends string>(
current: TableSort<TField>,
field: TField,
defaultDirection: SortDirection,
): TableSort<TField> {
if (current.field !== field) {
return { field, direction: defaultDirection };
}
return {
field,
direction: current.direction === "asc" ? "desc" : "asc",
};
}
export function sortBacklinkRows(
rows: BacklinksOverviewData["backlinks"],
sort: BacklinksTableSort,
) {
return rows.toSorted((left, right) => {
switch (sort.field) {
case "source":
return compareStrings(
left.domainFrom?.replace(/^www\./, "") ?? left.urlFrom,
right.domainFrom?.replace(/^www\./, "") ?? right.urlFrom,
sort.direction,
);
case "target":
return compareStrings(left.urlTo, right.urlTo, sort.direction);
case "anchor":
return compareStrings(
left.anchor ?? left.itemType,
right.anchor ?? right.itemType,
sort.direction,
);
case "linkAuthority":
return compareNumbers(left.rank, right.rank, sort.direction);
case "domainAuthority":
return compareNumbers(
left.domainFromRank,
right.domainFromRank,
sort.direction,
);
case "firstSeen":
return compareDates(left.firstSeen, right.firstSeen, sort.direction);
default:
return 0;
}
});
}
export function sortReferringDomainRows(
rows: BacklinksOverviewData["referringDomains"],
sort: ReferringDomainsTableSort,
) {
return rows.toSorted((left, right) => {
switch (sort.field) {
case "domain":
return compareStrings(left.domain, right.domain, sort.direction);
case "backlinks":
return compareNumbers(left.backlinks, right.backlinks, sort.direction);
case "referringPages":
return compareNumbers(
left.referringPages,
right.referringPages,
sort.direction,
);
case "rank":
return compareNumbers(left.rank, right.rank, sort.direction);
case "spamScore":
return compareNumbers(left.spamScore, right.spamScore, sort.direction);
case "firstSeen":
return compareDates(left.firstSeen, right.firstSeen, sort.direction);
case "issues":
return compareIssues(left, right, sort.direction);
default:
return 0;
}
});
}
export function sortTopPageRows(
rows: BacklinksOverviewData["topPages"],
sort: TopPagesTableSort,
) {
return rows.toSorted((left, right) => {
switch (sort.field) {
case "page":
return compareStrings(left.page, right.page, sort.direction);
case "backlinks":
return compareNumbers(left.backlinks, right.backlinks, sort.direction);
case "referringDomains":
return compareNumbers(
left.referringDomains,
right.referringDomains,
sort.direction,
);
case "rank":
return compareNumbers(left.rank, right.rank, sort.direction);
case "brokenBacklinks":
return compareNumbers(
left.brokenBacklinks,
right.brokenBacklinks,
sort.direction,
);
default:
return 0;
}
});
}
function compareNumbers(
left: number | null | undefined,
right: number | null | undefined,
direction: SortDirection,
) {
if (left == null && right == null) return 0;
if (left == null) return 1;
if (right == null) return -1;
return direction === "asc" ? left - right : right - left;
}
function compareStrings(
left: string | null | undefined,
right: string | null | undefined,
direction: SortDirection,
) {
if (!left && !right) return 0;
if (!left) return 1;
if (!right) return -1;
const result = left.toLowerCase().localeCompare(right.toLowerCase());
return direction === "asc" ? result : -result;
}
function compareDates(
left: string | null | undefined,
right: string | null | undefined,
direction: SortDirection,
) {
if (!left && !right) return 0;
if (!left) return 1;
if (!right) return -1;
const leftValue = Date.parse(left);
const rightValue = Date.parse(right);
return direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
}
function compareIssues(
left: BacklinksOverviewData["referringDomains"][number],
right: BacklinksOverviewData["referringDomains"][number],
direction: SortDirection,
) {
const backlinkComparison = compareNumbers(
left.brokenBacklinks,
right.brokenBacklinks,
direction,
);
if (backlinkComparison !== 0) {
return backlinkComparison;
}
return compareNumbers(left.brokenPages, right.brokenPages, direction);
}