Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export function RankTrackingDomainDetail({
showMobile,
config.domain,
config.locationName,
run?.lastCheckedAt,
)
}
onExportToSheets={() =>
Expand All @@ -279,6 +280,7 @@ export function RankTrackingDomainDetail({
showDesktop,
showMobile,
config.locationName,
run?.lastCheckedAt,
)
}
onCopyKeywords={() => {
Expand Down
22 changes: 22 additions & 0 deletions src/client/features/rank-tracking/RankTrackingTableParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,28 @@ export function csvChange(
return previous - current;
}

/**
* Formats an ISO-8601 timestamp as a sortable YYYY-MM-DD date string.
* Returns an empty string when the value is nullish so spreadsheet tools
* treat the cell as blank rather than showing an error.
*/
function formatCheckedAt(lastCheckedAt?: string | null): string {
if (!lastCheckedAt) return "";
try {
return new Date(lastCheckedAt).toISOString().slice(0, 10);
} catch {
return "";
}
}
Comment on lines +173 to +185

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Formats an ISO-8601 timestamp as a sortable YYYY-MM-DD date string.
* Returns an empty string when the value is nullish so spreadsheet tools
* treat the cell as blank rather than showing an error.
*/
function formatCheckedAt(lastCheckedAt?: string | null): string {
if (!lastCheckedAt) return "";
try {
return new Date(lastCheckedAt).toISOString().slice(0, 10);
} catch {
return "";
}
}
/**
* Formats an ISO-8601 timestamp as a sortable YYYY-MM-DD date string in the
* user's timezone (en-CA formats as YYYY-MM-DD), matching the "Last:" date
* shown in the detail header. Returns an empty string when the value is
* nullish or invalid so spreadsheet tools treat the cell as blank.
*/
function formatCheckedAt(lastCheckedAt?: string | null): string {
if (!lastCheckedAt) return "";
const date = new Date(lastCheckedAt);
return Number.isNaN(date.getTime()) ? "" : date.toLocaleDateString("en-CA");
}


export function buildRankTrackingExport(
sorted: RankTrackingRow[],
showDesktop: boolean,
showMobile: boolean,
locationName?: string | null,
lastCheckedAt?: string | null,
): { headers: string[]; rows: (string | number)[][] } {
const checkedAtValue = formatCheckedAt(lastCheckedAt);
const headers = [
"Keyword",
// Exports lack the table's tooltip, so name the city inline.
Expand All @@ -200,6 +216,7 @@ export function buildRankTrackingExport(
"Mobile SERP Features",
]
: []),
"Last checked at",
];
// Emit empty cells (not "Not ranking" strings) so Sheets infers a numeric
// column type and the user can sort by position.
Expand All @@ -224,6 +241,7 @@ export function buildRankTrackingExport(
row.mobile.serpFeatures.join(", "),
]
: []),
checkedAtValue,
]);
return { headers, rows };
}
Expand All @@ -233,12 +251,14 @@ export function exportRankTrackingToSheets(
showDesktop: boolean,
showMobile: boolean,
locationName?: string | null,
lastCheckedAt?: string | null,
) {
const { headers, rows } = buildRankTrackingExport(
sorted,
showDesktop,
showMobile,
locationName,
lastCheckedAt,
);
void exportTableToSheets({ headers, rows, feature: "rank_tracking" });
}
Expand All @@ -249,6 +269,7 @@ export function exportRankTrackingCsv(
showMobile: boolean,
domain: string,
locationName?: string | null,
lastCheckedAt?: string | null,
) {
if (sorted.length === 0) {
toast.error("No data to export");
Expand All @@ -259,6 +280,7 @@ export function exportRankTrackingCsv(
showDesktop,
showMobile,
locationName,
lastCheckedAt,
);
// CSV file download keeps cents-formatted CPC for human readability;
// clipboard/Sheets export uses raw numbers (see buildRankTrackingExport).
Expand Down