Refine rank tracking onboarding and add ahrefs backlinks filter (#253)

This commit is contained in:
Ben Senescu 2026-06-09 22:17:13 -04:00 committed by GitHub
parent c38b453ab0
commit 2db6fdec71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 283 additions and 110 deletions

View File

@ -106,8 +106,10 @@ function CompactRangeInput({
function BacklinksTabFilters({ function BacklinksTabFilters({
form, form,
showAhrefsDrFilter,
}: { }: {
form: BacklinksFiltersState["backlinks"]["form"]; form: BacklinksFiltersState["backlinks"]["form"];
showAhrefsDrFilter: boolean;
}) { }) {
return ( return (
<> <>
@ -133,6 +135,14 @@ function BacklinksTabFilters({
minName="minDomainRank" minName="minDomainRank"
maxName="maxDomainRank" maxName="maxDomainRank"
/> />
{showAhrefsDrFilter ? (
<FilterRangeInputs
form={form}
title="Ahrefs DR"
minName="minAhrefsDr"
maxName="maxAhrefsDr"
/>
) : null}
<FilterRangeInputs <FilterRangeInputs
form={form} form={form}
title="Link Authority" title="Link Authority"
@ -219,8 +229,10 @@ function BacklinksTabFilters({
function ReferringDomainsFilters({ function ReferringDomainsFilters({
form, form,
showAhrefsDrFilter,
}: { }: {
form: BacklinksFiltersState["domains"]["form"]; form: BacklinksFiltersState["domains"]["form"];
showAhrefsDrFilter: boolean;
}) { }) {
return ( return (
<> <>
@ -252,6 +264,14 @@ function ReferringDomainsFilters({
minName="minRank" minName="minRank"
maxName="maxRank" maxName="maxRank"
/> />
{showAhrefsDrFilter ? (
<FilterRangeInputs
form={form}
title="Ahrefs DR"
minName="minAhrefsDr"
maxName="maxAhrefsDr"
/>
) : null}
<FilterRangeInputs <FilterRangeInputs
form={form} form={form}
title="Spam Score" title="Spam Score"
@ -313,9 +333,13 @@ function TopPagesFilters({
export function BacklinksFilterPanel({ export function BacklinksFilterPanel({
activeTab, activeTab,
filters, filters,
showAhrefsDrFilter,
activeFilterCount,
}: { }: {
activeTab: BacklinksTab; activeTab: BacklinksTab;
filters: BacklinksFiltersState; filters: BacklinksFiltersState;
showAhrefsDrFilter: boolean;
activeFilterCount: number;
}) { }) {
const current = filters[activeTab]; const current = filters[activeTab];
@ -324,9 +348,9 @@ export function BacklinksFilterPanel({
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="text-sm font-semibold">Refine results</p> <p className="text-sm font-semibold">Refine results</p>
{current.activeFilterCount > 0 ? ( {activeFilterCount > 0 ? (
<span className="badge badge-xs badge-primary border-0 text-primary-content"> <span className="badge badge-xs badge-primary border-0 text-primary-content">
{current.activeFilterCount} active {activeFilterCount} active
</span> </span>
) : null} ) : null}
</div> </div>
@ -334,7 +358,7 @@ export function BacklinksFilterPanel({
type="button" type="button"
className="btn btn-xs btn-ghost gap-1" className="btn btn-xs btn-ghost gap-1"
onClick={current.reset} onClick={current.reset}
disabled={current.activeFilterCount === 0} disabled={activeFilterCount === 0}
> >
<RotateCcw className="size-3" /> <RotateCcw className="size-3" />
Clear all Clear all
@ -342,10 +366,16 @@ export function BacklinksFilterPanel({
</div> </div>
{activeTab === "backlinks" ? ( {activeTab === "backlinks" ? (
<BacklinksTabFilters form={filters.backlinks.form} /> <BacklinksTabFilters
form={filters.backlinks.form}
showAhrefsDrFilter={showAhrefsDrFilter}
/>
) : null} ) : null}
{activeTab === "domains" ? ( {activeTab === "domains" ? (
<ReferringDomainsFilters form={filters.domains.form} /> <ReferringDomainsFilters
form={filters.domains.form}
showAhrefsDrFilter={showAhrefsDrFilter}
/>
) : null} ) : null}
{activeTab === "pages" ? ( {activeTab === "pages" ? (
<TopPagesFilters form={filters.pages.form} /> <TopPagesFilters form={filters.pages.form} />

View File

@ -23,6 +23,10 @@ import {
BacklinksExportMenu, BacklinksExportMenu,
} from "./BacklinksToolbarMenus"; } from "./BacklinksToolbarMenus";
import { buildBacklinksTabExport } from "./export"; import { buildBacklinksTabExport } from "./export";
import {
filterBacklinkRows,
filterReferringDomainRows,
} from "./backlinksFiltering";
import type { BacklinksFiltersState } from "./useBacklinksFilters"; import type { BacklinksFiltersState } from "./useBacklinksFilters";
import { useAhrefsDomainRatings } from "./useAhrefsDomainRatings"; import { useAhrefsDomainRatings } from "./useAhrefsDomainRatings";
@ -101,21 +105,49 @@ export function BacklinksResultsCard({
exportTarget: string; exportTarget: string;
onTabChange: (tab: BacklinksSearchState["tab"]) => void; onTabChange: (tab: BacklinksSearchState["tab"]) => void;
}) { }) {
const currentFilterCount = filters[activeTab].activeFilterCount;
const exportTable = useMemo(
() => buildBacklinksTabExport({ tab: activeTab, rows: filteredData }),
[activeTab, filteredData],
);
const { const {
ratings: domainRatings, ratings: domainRatings,
isLoading: isLoadingRatings, isLoading: isLoadingRatings,
loadRatings, loadRatings,
} = useAhrefsDomainRatings(projectId); } = useAhrefsDomainRatings(projectId);
const showAhrefsDrFilter = domainRatings !== null && activeTab !== "pages";
const currentFilterCount = countVisibleFilters(
filters[activeTab].values,
showAhrefsDrFilter,
);
const visibleFilteredData = useMemo(
() => ({
backlinks: filterBacklinkRows(
filteredData.backlinks,
filters.backlinks.values,
domainRatings,
),
referringDomains: filterReferringDomainRows(
filteredData.referringDomains,
filters.domains.values,
domainRatings,
),
topPages: filteredData.topPages,
}),
[
domainRatings,
filteredData.backlinks,
filteredData.referringDomains,
filteredData.topPages,
filters.backlinks.values,
filters.domains.values,
],
);
const exportTable = useMemo(
() =>
buildBacklinksTabExport({ tab: activeTab, rows: visibleFilteredData }),
[activeTab, visibleFilteredData],
);
// Domains keyed by both tables that the DR column can enrich. The Referring // Domains keyed by both tables that the DR column can enrich. The Referring
// Domains list loads lazily, so this grows once that tab is opened. // Domains list loads lazily, so this grows once that tab is opened.
const ratableDomains = useMemo( const ratableDomains = useMemo(
() => collectRatableDomains(filteredData), () => collectRatableDomains(visibleFilteredData),
[filteredData], [visibleFilteredData],
); );
// Once the user has opted in, keep newly loaded domains enriched without a // Once the user has opted in, keep newly loaded domains enriched without a
// re-click (e.g. after switching to the lazily-loaded Referring Domains tab). // re-click (e.g. after switching to the lazily-loaded Referring Domains tab).
@ -152,7 +184,7 @@ export function BacklinksResultsCard({
<BacklinksExportMenu <BacklinksExportMenu
activeTab={activeTab} activeTab={activeTab}
exportTarget={exportTarget} exportTarget={exportTarget}
filteredData={filteredData} filteredData={visibleFilteredData}
headers={exportTable.headers} headers={exportTable.headers}
rows={exportTable.rows} rows={exportTable.rows}
/> />
@ -183,7 +215,12 @@ export function BacklinksResultsCard({
</div> </div>
{filters.showFilters ? ( {filters.showFilters ? (
<BacklinksFilterPanel activeTab={activeTab} filters={filters} /> <BacklinksFilterPanel
activeTab={activeTab}
filters={filters}
showAhrefsDrFilter={showAhrefsDrFilter}
activeFilterCount={currentFilterCount}
/>
) : null} ) : null}
<div className="p-4"> <div className="p-4">
@ -194,7 +231,7 @@ export function BacklinksResultsCard({
) : null} ) : null}
{activeTab === "backlinks" ? ( {activeTab === "backlinks" ? (
<BacklinksTable <BacklinksTable
rows={filteredData.backlinks} rows={visibleFilteredData.backlinks}
domainRatings={domainRatings} domainRatings={domainRatings}
/> />
) : null} ) : null}
@ -203,7 +240,7 @@ export function BacklinksResultsCard({
) : null} ) : null}
{activeTab === "domains" && !isTabLoading && !tabErrorMessage ? ( {activeTab === "domains" && !isTabLoading && !tabErrorMessage ? (
<ReferringDomainsTable <ReferringDomainsTable
rows={filteredData.referringDomains} rows={visibleFilteredData.referringDomains}
domainRatings={domainRatings} domainRatings={domainRatings}
/> />
) : null} ) : null}
@ -235,6 +272,21 @@ function collectRatableDomains(filteredData: {
]; ];
} }
function countVisibleFilters(
values: Record<string, string>,
showAhrefsDrFilter: boolean,
) {
return Object.entries(values).filter(([key, value]) => {
if (
!showAhrefsDrFilter &&
(key === "minAhrefsDr" || key === "maxAhrefsDr")
) {
return false;
}
return value.trim() !== "";
}).length;
}
function OverviewGrid({ function OverviewGrid({
data, data,
summaryStats, summaryStats,

View File

@ -3,6 +3,8 @@ export type BacklinksTabFilterValues = {
exclude: string; exclude: string;
minDomainRank: string; minDomainRank: string;
maxDomainRank: string; maxDomainRank: string;
minAhrefsDr: string;
maxAhrefsDr: string;
minLinkAuthority: string; minLinkAuthority: string;
maxLinkAuthority: string; maxLinkAuthority: string;
minSpamScore: string; minSpamScore: string;
@ -19,6 +21,8 @@ export type ReferringDomainsFilterValues = {
maxBacklinks: string; maxBacklinks: string;
minRank: string; minRank: string;
maxRank: string; maxRank: string;
minAhrefsDr: string;
maxAhrefsDr: string;
minSpamScore: string; minSpamScore: string;
maxSpamScore: string; maxSpamScore: string;
}; };
@ -39,6 +43,8 @@ export const EMPTY_BACKLINKS_FILTERS: BacklinksTabFilterValues = {
exclude: "", exclude: "",
minDomainRank: "", minDomainRank: "",
maxDomainRank: "", maxDomainRank: "",
minAhrefsDr: "",
maxAhrefsDr: "",
minLinkAuthority: "", minLinkAuthority: "",
maxLinkAuthority: "", maxLinkAuthority: "",
minSpamScore: "", minSpamScore: "",
@ -55,6 +61,8 @@ export const EMPTY_REFERRING_DOMAINS_FILTERS: ReferringDomainsFilterValues = {
maxBacklinks: "", maxBacklinks: "",
minRank: "", minRank: "",
maxRank: "", maxRank: "",
minAhrefsDr: "",
maxAhrefsDr: "",
minSpamScore: "", minSpamScore: "",
maxSpamScore: "", maxSpamScore: "",
}; };

View File

@ -1,10 +1,39 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { BacklinksOverviewData } from "./backlinksPageTypes"; import type { BacklinksOverviewData } from "./backlinksPageTypes";
import { EMPTY_REFERRING_DOMAINS_FILTERS } from "./backlinksFilterTypes"; import {
import { filterReferringDomainRows } from "./backlinksFiltering"; EMPTY_BACKLINKS_FILTERS,
EMPTY_REFERRING_DOMAINS_FILTERS,
} from "./backlinksFilterTypes";
import {
filterBacklinkRows,
filterReferringDomainRows,
} from "./backlinksFiltering";
type BacklinkRow = BacklinksOverviewData["backlinks"][number];
type ReferringDomainRow = BacklinksOverviewData["referringDomains"][number]; type ReferringDomainRow = BacklinksOverviewData["referringDomains"][number];
function makeBacklinkRow(overrides: Partial<BacklinkRow> = {}): BacklinkRow {
return {
urlFrom: "https://example.com/post",
urlTo: "https://target.example/page",
domainFrom: "example.com",
anchor: "Example",
itemType: "anchor",
rank: 10,
domainFromRank: 20,
pageFromRank: 10,
spamScore: 2,
relAttributes: [],
firstSeen: null,
lastSeen: null,
linksCount: 1,
isDofollow: true,
isLost: false,
isBroken: false,
...overrides,
};
}
function makeReferringDomainRow( function makeReferringDomainRow(
overrides: Partial<ReferringDomainRow> = {}, overrides: Partial<ReferringDomainRow> = {},
): ReferringDomainRow { ): ReferringDomainRow {
@ -21,6 +50,57 @@ function makeReferringDomainRow(
}; };
} }
describe("filterBacklinkRows", () => {
it("ignores Ahrefs DR range until ratings are loaded for the backlinks table", () => {
const rows = [
makeBacklinkRow({ domainFrom: "low.example" }),
makeBacklinkRow({ domainFrom: "high.example" }),
];
expect(
filterBacklinkRows(rows, {
...EMPTY_BACKLINKS_FILTERS,
minAhrefsDr: "50",
}),
).toEqual(rows);
});
it("filters by loaded Ahrefs DR range for the backlinks table", () => {
const rows = [
makeBacklinkRow({ domainFrom: "low.example" }),
makeBacklinkRow({ domainFrom: "www.high.example" }),
makeBacklinkRow({ domainFrom: "unknown.example" }),
];
const ratings = {
"low.example": 12,
"high.example": 64,
"unknown.example": null,
};
expect(
filterBacklinkRows(
rows,
{
...EMPTY_BACKLINKS_FILTERS,
minAhrefsDr: "50",
},
ratings,
),
).toEqual([rows[1], rows[2]]);
expect(
filterBacklinkRows(
rows,
{
...EMPTY_BACKLINKS_FILTERS,
maxAhrefsDr: "50",
},
ratings,
),
).toEqual([rows[0], rows[2]]);
});
});
describe("filterReferringDomainRows", () => { describe("filterReferringDomainRows", () => {
it("filters by spam score range", () => { it("filters by spam score range", () => {
const rows = [ const rows = [
@ -43,4 +123,53 @@ describe("filterReferringDomainRows", () => {
}), }),
).toEqual([rows[1], rows[2]]); ).toEqual([rows[1], rows[2]]);
}); });
it("ignores Ahrefs DR range until ratings are loaded for referring domains", () => {
const rows = [
makeReferringDomainRow({ domain: "low.example" }),
makeReferringDomainRow({ domain: "high.example" }),
];
expect(
filterReferringDomainRows(rows, {
...EMPTY_REFERRING_DOMAINS_FILTERS,
minAhrefsDr: "50",
}),
).toEqual(rows);
});
it("filters by loaded Ahrefs DR range for referring domains", () => {
const rows = [
makeReferringDomainRow({ domain: "low.example" }),
makeReferringDomainRow({ domain: "high.example" }),
makeReferringDomainRow({ domain: "unknown.example" }),
];
const ratings = {
"low.example": 12,
"high.example": 64,
"unknown.example": null,
};
expect(
filterReferringDomainRows(
rows,
{
...EMPTY_REFERRING_DOMAINS_FILTERS,
minAhrefsDr: "50",
},
ratings,
),
).toEqual([rows[1], rows[2]]);
expect(
filterReferringDomainRows(
rows,
{
...EMPTY_REFERRING_DOMAINS_FILTERS,
maxAhrefsDr: "50",
},
ratings,
),
).toEqual([rows[0], rows[2]]);
});
}); });

View File

@ -5,6 +5,7 @@ import type {
ReferringDomainsFilterValues, ReferringDomainsFilterValues,
TopPagesFilterValues, TopPagesFilterValues,
} from "./backlinksFilterTypes"; } from "./backlinksFilterTypes";
import type { DomainRatings } from "./useAhrefsDomainRatings";
function passesNumericFilter( function passesNumericFilter(
value: number | null | undefined, value: number | null | undefined,
@ -40,6 +41,7 @@ function passesTextFilter(
export function filterBacklinkRows( export function filterBacklinkRows(
rows: BacklinksOverviewData["backlinks"], rows: BacklinksOverviewData["backlinks"],
filters: BacklinksTabFilterValues, filters: BacklinksTabFilterValues,
domainRatings?: DomainRatings | null,
): BacklinksOverviewData["backlinks"] { ): BacklinksOverviewData["backlinks"] {
const includeTerms = parseTerms(filters.include); const includeTerms = parseTerms(filters.include);
const excludeTerms = parseTerms(filters.exclude); const excludeTerms = parseTerms(filters.exclude);
@ -58,6 +60,17 @@ export function filterBacklinkRows(
) )
) )
return false; return false;
if (
domainRatings &&
!passesNumericFilter(
row.domainFrom
? domainRatings[row.domainFrom.replace(/^www\./, "")]
: null,
filters.minAhrefsDr,
filters.maxAhrefsDr,
)
)
return false;
if ( if (
!passesNumericFilter( !passesNumericFilter(
row.rank, row.rank,
@ -90,6 +103,7 @@ export function filterBacklinkRows(
export function filterReferringDomainRows( export function filterReferringDomainRows(
rows: BacklinksOverviewData["referringDomains"], rows: BacklinksOverviewData["referringDomains"],
filters: ReferringDomainsFilterValues, filters: ReferringDomainsFilterValues,
domainRatings?: DomainRatings | null,
): BacklinksOverviewData["referringDomains"] { ): BacklinksOverviewData["referringDomains"] {
const includeTerms = parseTerms(filters.include); const includeTerms = parseTerms(filters.include);
const excludeTerms = parseTerms(filters.exclude); const excludeTerms = parseTerms(filters.exclude);
@ -107,6 +121,15 @@ export function filterReferringDomainRows(
return false; return false;
if (!passesNumericFilter(row.rank, filters.minRank, filters.maxRank)) if (!passesNumericFilter(row.rank, filters.minRank, filters.maxRank))
return false; return false;
if (
domainRatings &&
!passesNumericFilter(
row.domain ? domainRatings[row.domain] : null,
filters.minAhrefsDr,
filters.maxAhrefsDr,
)
)
return false;
if ( if (
!passesNumericFilter( !passesNumericFilter(
row.spamScore, row.spamScore,

View File

@ -5,11 +5,8 @@ import {
type RowSelectionState, type RowSelectionState,
type SortingState, type SortingState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { Loader2, AlertCircle, FileDown, Sheet, X } from "lucide-react"; import { Loader2, AlertCircle, X } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { buildCsv, downloadCsv } from "@/client/lib/csv";
import { exportTableToSheets } from "@/client/lib/exportToSheets";
import { captureClientEvent } from "@/client/lib/posthog";
import { getDomainKeywordSuggestions } from "@/serverFunctions/domain"; import { getDomainKeywordSuggestions } from "@/serverFunctions/domain";
import { addTrackingKeywords } from "@/serverFunctions/rank-tracking"; import { addTrackingKeywords } from "@/serverFunctions/rank-tracking";
import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { getStandardErrorMessage } from "@/client/lib/error-messages";
@ -18,11 +15,6 @@ import {
makeSelectionColumn, makeSelectionColumn,
useAppTable, useAppTable,
} from "@/client/components/table/AppDataTable"; } from "@/client/components/table/AppDataTable";
import {
TableBulkActionBar,
TableBulkActionButton,
TableBulkExportMenu,
} from "@/client/components/table/TableBulkActionBar";
import { SortableHeader } from "./RankTrackingColumns"; import { SortableHeader } from "./RankTrackingColumns";
import { import {
applyShiftRangeSelection, applyShiftRangeSelection,
@ -37,12 +29,6 @@ type SuggestedKeyword = {
}; };
const PRE_SELECT_COUNT = 20; const PRE_SELECT_COUNT = 20;
const SUGGESTED_KEYWORD_EXPORT_HEADERS = [
"Keyword",
"Position",
"Volume",
"Traffic",
];
const baseColumns: ColumnDef<SuggestedKeyword>[] = [ const baseColumns: ColumnDef<SuggestedKeyword>[] = [
{ {
@ -240,32 +226,6 @@ export function KeywordSuggestionStep({
addMutation.mutate(selectedKeywords); addMutation.mutate(selectedKeywords);
} }
}; };
const selectedSuggestionRows = table
.getSelectedRowModel()
.rows.map((row) => row.original);
const selectedSuggestionExportRows = selectedSuggestionRows.map((row) => [
row.keyword,
row.position ?? "",
row.searchVolume ?? "",
row.traffic ?? "",
]);
const handleExportSelectionToSheets = () => {
void exportTableToSheets({
headers: SUGGESTED_KEYWORD_EXPORT_HEADERS,
rows: selectedSuggestionExportRows,
feature: "rank_tracking",
});
};
const handleExportSelectionCsv = () => {
downloadCsv(
`rank-tracking-suggestions-${domain}.csv`,
buildCsv(SUGGESTED_KEYWORD_EXPORT_HEADERS, selectedSuggestionExportRows),
);
captureClientEvent("rank_tracking:suggestions_export_csv", {
result_count: selectedSuggestionRows.length,
scope: "selection",
});
};
const sectionHeader = (title: string) => ( const sectionHeader = (title: string) => (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -301,18 +261,12 @@ export function KeywordSuggestionStep({
<div className="flex flex-col items-center justify-center gap-3 py-16"> <div className="flex flex-col items-center justify-center gap-3 py-16">
<AlertCircle className="size-8 text-error" /> <AlertCircle className="size-8 text-error" />
<p className="text-xs text-base-content/50"> <p className="text-xs text-base-content/50">
You can try again or add keywords manually later. You can skip this step and add keywords manually later.
</p> </p>
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-2">
<button className="btn btn-ghost btn-sm" onClick={onClose}> <button className="btn btn-primary btn-sm" onClick={onClose}>
Skip Skip
</button> </button>
<button
className="btn btn-primary btn-sm"
onClick={() => suggestionsQuery.refetch()}
>
Try Again
</button>
</div> </div>
</div> </div>
</> </>
@ -330,7 +284,7 @@ export function KeywordSuggestionStep({
add keywords manually. add keywords manually.
</p> </p>
<button className="btn btn-primary btn-sm mt-2" onClick={onClose}> <button className="btn btn-primary btn-sm mt-2" onClick={onClose}>
Continue Skip
</button> </button>
</div> </div>
</> </>
@ -365,44 +319,25 @@ export function KeywordSuggestionStep({
/> />
<div className="flex items-center justify-between gap-3 pt-1"> <div className="flex items-center justify-between gap-3 pt-1">
<button className="btn btn-ghost btn-sm" onClick={onClose}> <p className="text-xs text-base-content/60">
Skip {selectedCount} of {data.length} selected
</button> </p>
<TableBulkActionBar <div className="flex items-center gap-2">
selectedCount={selectedCount} <button className="btn btn-ghost btn-sm" onClick={onClose}>
selectedLabel={`of ${data.length} selected`} Skip
onClear={() => table.resetRowSelection()} </button>
placement="inline" <button
actions={ type="button"
<div className="flex items-center px-1.5"> className="btn btn-primary btn-sm"
<TableBulkActionButton onClick={handleAdd}
icon={ disabled={addMutation.isPending || selectedCount === 0}
addMutation.isPending ? ( >
<Loader2 className="size-3.5 animate-spin" /> {addMutation.isPending && (
) : undefined <Loader2 className="size-3.5 animate-spin" />
} )}
onClick={handleAdd} Save Keyword{selectedCount !== 1 ? "s" : ""}
disabled={addMutation.isPending} </button>
> </div>
Add Keyword{selectedCount !== 1 ? "s" : ""}
</TableBulkActionButton>
<TableBulkExportMenu
actions={[
{
label: "Export to Sheets",
icon: <Sheet className="size-4" />,
onClick: handleExportSelectionToSheets,
},
{
label: "Export CSV",
icon: <FileDown className="size-4" />,
onClick: handleExportSelectionCsv,
},
]}
/>
</div>
}
/>
</div> </div>
</div> </div>
); );

View File

@ -256,7 +256,6 @@ function RankTrackingDomainDetailInner({
{/* Portfolio overview */} {/* Portfolio overview */}
{(rows?.length ?? 0) > 0 && ( {(rows?.length ?? 0) > 0 && (
<RankTrackingOverview <RankTrackingOverview
rows={rows ?? []}
device={activeDevice} device={activeDevice}
projectId={projectId} projectId={projectId}
configId={config.id} configId={config.id}

View File

@ -11,7 +11,6 @@ import {
} from "recharts"; } from "recharts";
import type { TooltipContentProps } from "recharts"; import type { TooltipContentProps } from "recharts";
import { getRankConfigTrend } from "@/serverFunctions/rank-tracking"; import { getRankConfigTrend } from "@/serverFunctions/rank-tracking";
import type { RankTrackingRow } from "@/types/schemas/rank-tracking";
import { import {
formatDateTick, formatDateTick,
TrendRangeToggle, TrendRangeToggle,
@ -32,12 +31,10 @@ interface PayloadEntry {
} }
export function RankTrackingOverview({ export function RankTrackingOverview({
rows,
device, device,
projectId, projectId,
configId, configId,
}: { }: {
rows: RankTrackingRow[];
device: "desktop" | "mobile"; device: "desktop" | "mobile";
projectId: string; projectId: string;
configId: string; configId: string;