keywords: consolidate export buttons into dropdown (#147)

This commit is contained in:
Ben Senescu 2026-05-04 14:04:44 -04:00 committed by GitHub
parent c1a9c1f57d
commit a42fe4d350
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 92 additions and 35 deletions

View File

@ -1,12 +1,15 @@
import { import {
ChevronDown,
Download,
FileDown, FileDown,
Globe, Globe,
RotateCcw, RotateCcw,
Save, Save,
Sheet,
SlidersHorizontal, SlidersHorizontal,
} from "lucide-react"; } from "lucide-react";
import { ExportToSheetsButton } from "@/client/components/table/ExportToSheetsButton";
import { KEYWORD_RESEARCH_HEADERS } from "@/client/features/keywords/state/keywordControllerActions"; import { KEYWORD_RESEARCH_HEADERS } from "@/client/features/keywords/state/keywordControllerActions";
import { exportTableToSheets } from "@/client/lib/exportToSheets";
import { import {
AreaTrendChart, AreaTrendChart,
KeywordRow, KeywordRow,
@ -121,6 +124,15 @@ function DesktopTableCard({ controller }: Props) {
? `Showing ${filteredRows.length} of ${rows.length} keywords` ? `Showing ${filteredRows.length} of ${rows.length} keywords`
: `Showing ${filteredRows.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 ( return (
<div className="flex-1 flex flex-col min-w-0 border border-base-300 rounded-xl bg-base-100 overflow-hidden"> <div className="flex-1 flex flex-col min-w-0 border border-base-300 rounded-xl bg-base-100 overflow-hidden">
<div className="shrink-0 flex items-center gap-2 px-4 py-2 border-b border-base-300"> <div className="shrink-0 flex items-center gap-2 px-4 py-2 border-b border-base-300">
@ -149,20 +161,34 @@ function DesktopTableCard({ controller }: Props) {
<Save className="size-3.5" /> <Save className="size-3.5" />
<span className="hidden lg:inline">Save Keywords</span> <span className="hidden lg:inline">Save Keywords</span>
</button> </button>
<ExportToSheetsButton <div className="dropdown dropdown-end">
headers={KEYWORD_RESEARCH_HEADERS} <div
rows={sheetsExportRows} tabIndex={0}
feature="keyword_research" role="button"
className="btn-sm" className={`btn btn-ghost btn-sm gap-1 ${!canExport ? "btn-disabled" : ""}`}
/> >
<button <Download className="size-3.5" />
className="btn btn-ghost btn-sm gap-1" <span className="hidden lg:inline">Export</span>
onClick={controller.exportCsv} <ChevronDown className="size-3 opacity-60" />
disabled={filteredRows.length === 0} </div>
> <ul
<FileDown className="size-3.5" /> tabIndex={0}
<span className="hidden lg:inline">Export CSV</span> className="dropdown-content z-10 menu p-2 shadow-lg bg-base-100 border border-base-300 rounded-box w-56"
</button> >
<li>
<button onClick={handleExportToSheets} disabled={!canExport}>
<Sheet className="size-4" />
Export to Google Sheets
</button>
</li>
<li>
<button onClick={controller.exportCsv} disabled={!canExport}>
<FileDown className="size-4" />
Export CSV
</button>
</li>
</ul>
</div>
</div> </div>
{showFilters ? <DesktopFilters controller={controller} /> : null} {showFilters ? <DesktopFilters controller={controller} /> : null}
@ -330,9 +356,9 @@ function DesktopSerpPanel({ controller }: Props) {
: "Last 12 available months"; : "Last 12 available months";
return ( return (
<div className="order-1 xl:order-2 flex flex-col min-w-0 gap-2 xl:flex-1"> <div className="order-1 xl:order-2 flex flex-col min-w-0 gap-2 xl:flex-1 xl:overflow-y-auto">
{overviewKeyword && overviewKeyword.trend.length > 0 ? ( {overviewKeyword && overviewKeyword.trend.length > 0 ? (
<div className="shrink-0 border border-base-300 rounded-xl bg-base-100 px-4 py-3"> <div className="shrink-0 overflow-hidden border border-base-300 rounded-xl bg-base-100 px-4 py-3">
<h4 className="text-sm font-semibold mb-1"> <h4 className="text-sm font-semibold mb-1">
Search Trends{" "} Search Trends{" "}
<span className="font-normal text-base-content/50"> <span className="font-normal text-base-content/50">
@ -343,7 +369,7 @@ function DesktopSerpPanel({ controller }: Props) {
</div> </div>
) : null} ) : null}
<div className="flex-1 flex flex-col overflow-hidden border border-base-300 rounded-xl bg-base-100"> <div className="flex flex-col overflow-hidden border border-base-300 rounded-xl bg-base-100">
<div className="shrink-0 px-4 py-3 border-b border-base-300"> <div className="shrink-0 px-4 py-3 border-b border-base-300">
<h3 className="text-sm font-semibold flex items-center gap-1.5"> <h3 className="text-sm font-semibold flex items-center gap-1.5">
<Globe className="size-3.5" /> <Globe className="size-3.5" />
@ -355,7 +381,7 @@ function DesktopSerpPanel({ controller }: Props) {
) : null} ) : null}
</h3> </h3>
</div> </div>
<div className="flex-1 overflow-y-auto p-4"> <div className="p-4">
<SerpAnalysisCard <SerpAnalysisCard
items={controller.serpResults} items={controller.serpResults}
keyword={controller.activeSerpKeyword} keyword={controller.activeSerpKeyword}

View File

@ -1,6 +1,14 @@
import { FileDown, RotateCcw, Save, SlidersHorizontal } from "lucide-react"; import {
import { ExportToSheetsButton } from "@/client/components/table/ExportToSheetsButton"; ChevronDown,
Download,
FileDown,
RotateCcw,
Save,
Sheet,
SlidersHorizontal,
} from "lucide-react";
import { KEYWORD_RESEARCH_HEADERS } from "@/client/features/keywords/state/keywordControllerActions"; import { KEYWORD_RESEARCH_HEADERS } from "@/client/features/keywords/state/keywordControllerActions";
import { exportTableToSheets } from "@/client/lib/exportToSheets";
import { import {
KeywordCard, KeywordCard,
SerpAnalysisCard, SerpAnalysisCard,
@ -76,6 +84,15 @@ function MobileKeywordCards({ controller }: Props) {
? `Showing ${filteredRows.length} of ${rows.length}` ? `Showing ${filteredRows.length} of ${rows.length}`
: `Showing ${filteredRows.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 ( return (
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
{controller.showApproximateMatchNotice ? ( {controller.showApproximateMatchNotice ? (
@ -113,20 +130,34 @@ function MobileKeywordCards({ controller }: Props) {
> >
<Save className="size-3.5" /> <Save className="size-3.5" />
</button> </button>
<ExportToSheetsButton <div className="dropdown dropdown-end">
headers={KEYWORD_RESEARCH_HEADERS} <div
rows={sheetsExportRows} tabIndex={0}
feature="keyword_research" role="button"
iconOnly className={`btn btn-ghost btn-xs gap-1 ${!canExport ? "btn-disabled" : ""}`}
/> aria-label="Export"
<button >
className="btn btn-ghost btn-xs" <Download className="size-3.5" />
onClick={controller.exportCsv} <ChevronDown className="size-3 opacity-60" />
disabled={filteredRows.length === 0} </div>
title="Download CSV" <ul
> tabIndex={0}
<FileDown className="size-3.5" /> className="dropdown-content z-10 menu p-2 shadow-lg bg-base-100 border border-base-300 rounded-box w-56"
</button> >
<li>
<button onClick={handleExportToSheets} disabled={!canExport}>
<Sheet className="size-4" />
Export to Google Sheets
</button>
</li>
<li>
<button onClick={controller.exportCsv} disabled={!canExport}>
<FileDown className="size-4" />
Export CSV
</button>
</li>
</ul>
</div>
</div> </div>
{showFilters ? <MobileFilters controller={controller} /> : null} {showFilters ? <MobileFilters controller={controller} /> : null}