import { createFileRoute } from "@tanstack/react-router"; import { useState } from "react"; import { toast } from "sonner"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getSavedKeywords, removeSavedKeyword, } from "@/serverFunctions/keywords"; import { Trash2, Download, Search, Loader2, AlertCircle } from "lucide-react"; import { buildCsv, downloadCsv } from "@/client/lib/csv"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; export const Route = createFileRoute("/p/$projectId/saved")({ component: SavedKeywordsPage, }); function SavedKeywordsPage() { const { projectId } = Route.useParams(); const queryClient = useQueryClient(); const [removeError, setRemoveError] = useState(null); const [removingId, setRemovingId] = useState(null); const { data: savedKeywordsData, isLoading } = useQuery({ queryKey: ["savedKeywords", projectId], queryFn: () => getSavedKeywords({ data: { projectId } }), }); const savedKeywords = savedKeywordsData?.rows ?? []; const removeMutation = useMutation({ mutationFn: (savedKeywordId: string) => removeSavedKeyword({ data: { savedKeywordId } }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["savedKeywords", projectId], }); toast.success("Keyword removed"); }, onError: (error) => { setRemoveError(getStandardErrorMessage(error, "Remove failed.")); }, }); const handleRemoveKeyword = (savedKeywordId: string) => { setRemoveError(null); setRemovingId(savedKeywordId); removeMutation.mutate(savedKeywordId, { onSettled: () => { setRemovingId((current) => current === savedKeywordId ? null : current, ); }, }); }; const exportCsv = () => { if (savedKeywords.length === 0) { toast.error("No keywords to export"); return; } const headers = [ "Keyword", "Volume", "CPC", "Competition", "Difficulty", "Intent", "Fetched At", ]; const csvRows = savedKeywords.map((kw) => [ kw.keyword, kw.searchVolume ?? "", kw.cpc?.toFixed(2) ?? "", kw.competition?.toFixed(2) ?? "", kw.keywordDifficulty ?? "", kw.intent ?? "", kw.fetchedAt ?? "", ]); const csv = buildCsv(headers, csvRows); downloadCsv("saved-keywords.csv", csv); }; return ( ); } function SavedKeywordsContent({ isLoading, removeError, removingId, savedKeywords, onExportCsv, onRemoveKeyword, }: { isLoading: boolean; removeError: string | null; removingId: string | null; savedKeywords: Array<{ id: string; keyword: string; searchVolume: number | null; cpc: number | null; competition: number | null; keywordDifficulty: number | null; intent: string | null; fetchedAt: string | null; }>; onExportCsv: () => void; onRemoveKeyword: (savedKeywordId: string) => void; }) { return (

Saved Keywords

Keywords you've saved from keyword research.

{savedKeywords.length > 0 && ( )}
{isLoading ? (
{Array.from({ length: 8 }).map((_, index) => (
))}
) : savedKeywords.length === 0 ? (

No saved keywords yet. Use the Keyword Research page to find and save keywords.

) : (
{removeError ? (
{removeError}
) : null}

{savedKeywords.length} saved keyword {savedKeywords.length !== 1 ? "s" : ""}

)}
); } function SavedKeywordsTable({ rows, removingId, onRemoveKeyword, }: { rows: Array<{ id: string; keyword: string; searchVolume: number | null; cpc: number | null; competition: number | null; keywordDifficulty: number | null; intent: string | null; fetchedAt: string | null; }>; removingId: string | null; onRemoveKeyword: (savedKeywordId: string) => void; }) { return (
{rows.map((kw) => ( ))}
Keyword Volume CPC Competition Difficulty Intent Last Fetched
{kw.keyword} {formatNumber(kw.searchVolume)} {kw.cpc == null ? "-" : `$${kw.cpc.toFixed(2)}`} {kw.competition == null ? "-" : kw.competition.toFixed(2)} {kw.intent ?? "?"} {kw.fetchedAt ? new Date(kw.fetchedAt).toLocaleDateString() : "-"}
); } function DifficultyBadge({ value }: { value: number | null }) { if (value == null) return -; if (value < 30) return {value}; if (value <= 60) return {value}; return {value}; } function formatNumber(value: number | null | undefined) { if (value == null) return "-"; return new Intl.NumberFormat().format(value); }