From 45234e6945b64d832db64c1bdec80773aae5fccb Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Fri, 22 May 2026 22:49:05 -0400 Subject: [PATCH] Fix backlink tab states (#218) * Fix tab active state wiring across result views * Add spam filters to referring domains --- .../components/BrandLookupResults.tsx | 8 ++- .../features/audit/results/ResultsView.tsx | 55 ++++++++-------- .../backlinks/BacklinksFilterPanel.tsx | 9 ++- .../features/backlinks/BacklinksPage.tsx | 13 ++++ .../backlinks/BacklinksPageContent.tsx | 4 +- .../backlinks/BacklinksPageSections.tsx | 63 ++++++++++--------- .../backlinks/backlinksFilterTypes.ts | 4 ++ .../backlinks/backlinksFiltering.test.ts | 46 ++++++++++++++ .../features/backlinks/backlinksFiltering.ts | 8 +++ .../_project/p/$projectId/audit/index.tsx | 4 ++ 10 files changed, 153 insertions(+), 61 deletions(-) create mode 100644 src/client/features/backlinks/backlinksFiltering.test.ts diff --git a/src/client/features/ai-search/components/BrandLookupResults.tsx b/src/client/features/ai-search/components/BrandLookupResults.tsx index 553f38e..a9896aa 100644 --- a/src/client/features/ai-search/components/BrandLookupResults.tsx +++ b/src/client/features/ai-search/components/BrandLookupResults.tsx @@ -277,6 +277,8 @@ function CitationTabsCard({ result }: { result: BrandLookupResult }) { const canExport = exportTable.rows.length > 0; const currentFilterCount = filters[activeTab].activeFilterCount; + const queriesActive = activeTab === "queries"; + const pagesActive = activeTab === "pages"; return (
@@ -285,7 +287,8 @@ function CitationTabsCard({ result }: { result: BrandLookupResult }) { + ); + })} ) : (

Pages ({pageCount})

diff --git a/src/client/features/backlinks/BacklinksFilterPanel.tsx b/src/client/features/backlinks/BacklinksFilterPanel.tsx index fc908cc..2d28217 100644 --- a/src/client/features/backlinks/BacklinksFilterPanel.tsx +++ b/src/client/features/backlinks/BacklinksFilterPanel.tsx @@ -239,7 +239,7 @@ function ReferringDomainsFilters({ /> -
+
+
); diff --git a/src/client/features/backlinks/BacklinksPage.tsx b/src/client/features/backlinks/BacklinksPage.tsx index 1a23517..c3a2fb0 100644 --- a/src/client/features/backlinks/BacklinksPage.tsx +++ b/src/client/features/backlinks/BacklinksPage.tsx @@ -65,6 +65,18 @@ export function BacklinksPage({ }, [navigate], ); + const handleResultTabChange = useCallback( + (tab: BacklinksSearchState["tab"]) => { + navigate({ + search: (prev) => ({ + ...prev, + tab: tab === "backlinks" ? undefined : tab, + }), + replace: true, + }); + }, + [navigate], + ); const searchTabs = useSearchTabNavigation({ storageKey: `backlinks:${projectId}`, urlInput: urlTabInput, @@ -134,6 +146,7 @@ export function BacklinksPage({ topPages={topPagesQuery.data} onRemoveHistoryItem={removeHistoryItem} onRetryOverview={() => void overviewQuery.refetch()} + onTabChange={handleResultTabChange} searchTabs={ searchState.target ? { diff --git a/src/client/features/backlinks/BacklinksPageContent.tsx b/src/client/features/backlinks/BacklinksPageContent.tsx index 35c2fcc..23c9a6b 100644 --- a/src/client/features/backlinks/BacklinksPageContent.tsx +++ b/src/client/features/backlinks/BacklinksPageContent.tsx @@ -47,6 +47,7 @@ type BacklinksBodyProps = { topPages: BacklinksTopPagesData | undefined; onRemoveHistoryItem: (timestamp: number) => void; onRetryOverview: () => void; + onTabChange: (tab: BacklinksSearchState["tab"]) => void; searchTabs: { activeTabId: string | null; tabs: SearchTab[]; @@ -73,6 +74,7 @@ export function BacklinksBody({ topPages, onRemoveHistoryItem, onRetryOverview, + onTabChange, searchTabs, }: BacklinksBodyProps) { const mergedData = useMemo( @@ -179,7 +181,6 @@ export function BacklinksBody({ summaryStats={summaryStats} /> ); diff --git a/src/client/features/backlinks/BacklinksPageSections.tsx b/src/client/features/backlinks/BacklinksPageSections.tsx index 92b4f7e..8675511 100644 --- a/src/client/features/backlinks/BacklinksPageSections.tsx +++ b/src/client/features/backlinks/BacklinksPageSections.tsx @@ -22,6 +22,15 @@ import { import { buildBacklinksTabExport, exportBacklinksTabCsv } from "./export"; import type { BacklinksFiltersState } from "./useBacklinksFilters"; +const BACKLINKS_RESULTS_TABS: Array<{ + tab: BacklinksSearchState["tab"]; + label: string; +}> = [ + { tab: "backlinks", label: "Backlinks" }, + { tab: "domains", label: "Referring Domains" }, + { tab: "pages", label: "Top Pages" }, +]; + export function BacklinksOverviewPanels({ projectId, data, @@ -66,15 +75,14 @@ export function BacklinksOverviewPanels({ } export function BacklinksResultsCard({ - projectId, activeTab, filteredData, filters, isTabLoading, tabErrorMessage, exportTarget, + onTabChange, }: { - projectId: string; activeTab: BacklinksSearchState["tab"]; filteredData: { backlinks: BacklinksOverviewData["backlinks"]; @@ -85,6 +93,7 @@ export function BacklinksResultsCard({ isTabLoading: boolean; tabErrorMessage: string | null; exportTarget: string; + onTabChange: (tab: BacklinksSearchState["tab"]) => void; }) { const currentFilterCount = filters[activeTab].activeFilterCount; const exportTable = useMemo( @@ -97,19 +106,15 @@ export function BacklinksResultsCard({
- - Backlinks - - - Referring Domains - - - Top Pages - + {BACKLINKS_RESULTS_TABS.map(({ label, tab }) => ( + + ))}

{TAB_DESCRIPTIONS[activeTab]} @@ -276,30 +281,28 @@ function TrendCard({ } function TabLink({ - projectId, activeTab, - children, + label, + onSelect, tab, }: { - projectId: string; activeTab: BacklinksSearchState["tab"]; - children: string; + label: string; + onSelect: (tab: BacklinksSearchState["tab"]) => void; tab: BacklinksSearchState["tab"]; }) { + const isActive = activeTab === tab; + return ( - ({ - ...prev, - tab: tab === "backlinks" ? undefined : tab, - })} - replace + ); } diff --git a/src/client/features/backlinks/backlinksFilterTypes.ts b/src/client/features/backlinks/backlinksFilterTypes.ts index 0091732..cca057f 100644 --- a/src/client/features/backlinks/backlinksFilterTypes.ts +++ b/src/client/features/backlinks/backlinksFilterTypes.ts @@ -19,6 +19,8 @@ export type ReferringDomainsFilterValues = { maxBacklinks: string; minRank: string; maxRank: string; + minSpamScore: string; + maxSpamScore: string; }; export type TopPagesFilterValues = { @@ -53,6 +55,8 @@ export const EMPTY_REFERRING_DOMAINS_FILTERS: ReferringDomainsFilterValues = { maxBacklinks: "", minRank: "", maxRank: "", + minSpamScore: "", + maxSpamScore: "", }; export const EMPTY_TOP_PAGES_FILTERS: TopPagesFilterValues = { diff --git a/src/client/features/backlinks/backlinksFiltering.test.ts b/src/client/features/backlinks/backlinksFiltering.test.ts new file mode 100644 index 0000000..dcded4a --- /dev/null +++ b/src/client/features/backlinks/backlinksFiltering.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import type { BacklinksOverviewData } from "./backlinksPageTypes"; +import { EMPTY_REFERRING_DOMAINS_FILTERS } from "./backlinksFilterTypes"; +import { filterReferringDomainRows } from "./backlinksFiltering"; + +type ReferringDomainRow = BacklinksOverviewData["referringDomains"][number]; + +function makeReferringDomainRow( + overrides: Partial = {}, +): ReferringDomainRow { + return { + domain: "example.com", + backlinks: 10, + referringPages: 5, + rank: 20, + spamScore: 2, + firstSeen: null, + brokenBacklinks: 0, + brokenPages: 0, + ...overrides, + }; +} + +describe("filterReferringDomainRows", () => { + it("filters by spam score range", () => { + const rows = [ + makeReferringDomainRow({ domain: "clean.example", spamScore: 1 }), + makeReferringDomainRow({ domain: "risky.example", spamScore: 7 }), + makeReferringDomainRow({ domain: "unknown.example", spamScore: null }), + ]; + + expect( + filterReferringDomainRows(rows, { + ...EMPTY_REFERRING_DOMAINS_FILTERS, + maxSpamScore: "3", + }), + ).toEqual([rows[0], rows[2]]); + + expect( + filterReferringDomainRows(rows, { + ...EMPTY_REFERRING_DOMAINS_FILTERS, + minSpamScore: "3", + }), + ).toEqual([rows[1], rows[2]]); + }); +}); diff --git a/src/client/features/backlinks/backlinksFiltering.ts b/src/client/features/backlinks/backlinksFiltering.ts index 9bafa80..cdf2737 100644 --- a/src/client/features/backlinks/backlinksFiltering.ts +++ b/src/client/features/backlinks/backlinksFiltering.ts @@ -107,6 +107,14 @@ export function filterReferringDomainRows( return false; if (!passesNumericFilter(row.rank, filters.minRank, filters.maxRank)) return false; + if ( + !passesNumericFilter( + row.spamScore, + filters.minSpamScore, + filters.maxSpamScore, + ) + ) + return false; return true; }); } diff --git a/src/routes/_project/p/$projectId/audit/index.tsx b/src/routes/_project/p/$projectId/audit/index.tsx index 14b38db..1d60f2e 100644 --- a/src/routes/_project/p/$projectId/audit/index.tsx +++ b/src/routes/_project/p/$projectId/audit/index.tsx @@ -56,6 +56,7 @@ function SiteAuditPage() { auditId={auditId} tab={tab} onBack={() => setSearchParams({ auditId: undefined })} + onTabChange={(nextTab) => setSearchParams({ tab: nextTab })} /> ); } @@ -65,11 +66,13 @@ function AuditDetail({ auditId, tab, onBack, + onTabChange, }: { projectId: string; auditId: string; tab: string; onBack: () => void; + onTabChange: (tab: "pages" | "performance") => void; }) { const statusQuery = useQuery({ queryKey: ["audit-status", projectId, auditId], @@ -178,6 +181,7 @@ function AuditDetail({ projectId={projectId} data={resultsQuery.data} tab={tab} + onTabChange={onTabChange} /> )}