import {
ChevronDown,
Download,
FileDown,
Globe,
RotateCcw,
Save,
Sheet,
SlidersHorizontal,
} from "lucide-react";
import { KEYWORD_RESEARCH_HEADERS } from "@/client/features/keywords/state/keywordControllerActions";
import { exportTableToSheets } from "@/client/lib/exportToSheets";
import {
AreaTrendChart,
OverviewStats,
SerpAnalysisCard,
} from "@/client/features/keywords/components";
import type { KeywordResearchRow } from "@/types/keywords";
import type { KeywordResearchControllerState } from "./types";
import {
FilterRangeInputs,
FilterTextInput,
} from "./keywordResearchDesktopFilters";
import { KeywordResearchDesktopTable } from "./KeywordResearchDesktopTable";
import {
KeywordResearchPagination,
useKeywordResearchPagination,
} from "./KeywordResearchPagination";
const MONTH_SHORT_LABELS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
] as const;
function formatTrendRangeLabel(trend: KeywordResearchRow["trend"]): string {
if (trend.length === 0) return "Last 12 available months";
const sorted = trend.toSorted(
(a, b) => a.year * 100 + a.month - (b.year * 100 + b.month),
);
const last12 = sorted.slice(-12);
const start = last12[0];
const end = last12[last12.length - 1];
const toLabel = (month: number, year: number) => {
const monthLabel = MONTH_SHORT_LABELS[month - 1] ?? `M${month}`;
return `${monthLabel} ${year}`;
};
const startLabel = toLabel(start.month, start.year);
const endLabel = toLabel(end.month, end.year);
return startLabel === endLabel ? startLabel : `${startLabel} - ${endLabel}`;
}
type Props = {
controller: KeywordResearchControllerState;
};
export function KeywordResearchDesktopResults({ controller }: Props) {
return (
);
}
function DesktopKeywordPanel({ controller }: Props) {
const {
lastResultSource,
lastUsedFallback,
searchedKeyword,
showApproximateMatchNotice,
} = controller;
return (
{showApproximateMatchNotice ? (
No exact match for{" "}
"{searchedKeyword}". Showing
closest related keywords instead.
{lastUsedFallback ? (
{" "}
Source: {lastResultSource} fallback.
) : null}
) : null}
{controller.overviewKeyword ? (
) : null}
);
}
function DesktopTableCard({ controller }: Props) {
const {
activeFilterCount,
filteredRows,
rows,
selectedRows,
sheetsExportRows,
showFilters,
} = controller;
const { page, pageSize, pageRows, setPage, setPageSize } =
useKeywordResearchPagination(filteredRows);
const keywordCountLabel =
selectedRows.size > 0
? `${selectedRows.size} of ${filteredRows.length} selected`
: activeFilterCount > 0
? `Showing ${filteredRows.length} of ${rows.length} keywords`
: `Showing ${filteredRows.length} keywords`;
const canExport = filteredRows.length > 0;
const handleExportToSheets = () => {
void exportTableToSheets({
headers: KEYWORD_RESEARCH_HEADERS,
rows: sheetsExportRows,
feature: "keyword_research",
});
};
return (
{keywordCountLabel}
Export
-
-
{showFilters ?
: null}
{filteredRows.length > 0 ? (
) : null}
);
}
function DesktopFilters({ controller }: Props) {
const { activeFilterCount, filtersForm } = controller;
return (
Refine table results
{activeFilterCount > 0 ? (
{activeFilterCount} active
) : null}
);
}
function DesktopSerpPanel({ controller }: Props) {
const { overviewKeyword } = controller;
const trendRangeLabel = overviewKeyword
? formatTrendRangeLabel(overviewKeyword.trend)
: "Last 12 available months";
return (
{overviewKeyword && overviewKeyword.trend.length > 0 ? (
Search Trends{" "}
{trendRangeLabel}
) : null}
SERP Analysis
{controller.activeSerpKeyword ? (
: {controller.activeSerpKeyword}
) : null}
void controller.serpQuery.refetch()}
page={controller.serpPage}
pageSize={controller.SERP_PAGE_SIZE}
onPageChange={controller.setSerpPage}
/>
);
}