import { useMemo, useState } from "react"; import { toast } from "sonner"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { AutumnProvider, useCustomer } from "autumn-js/react"; import { getLatestRankResults, getRankPositionMatrix, estimateRankCheckCost, } from "@/serverFunctions/rank-tracking"; import { AlertTriangle, ArrowLeft } from "lucide-react"; import { useSession } from "@/lib/auth-client"; import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection"; import { captureClientEvent } from "@/client/lib/posthog"; import { FreePlanAlert } from "./FreePlanAlert"; import { RankTrackingDetailHeader } from "./RankTrackingDetailHeader"; import { RankTrackingOverview } from "./RankTrackingOverview"; import { RankTrackingTable } from "./RankTrackingTable"; import { countMatrixRuns, RankTrackingHistoryMatrix, } from "./RankTrackingHistoryMatrix"; import { RankTrackingTableToolbar } from "./RankTrackingTableToolbar"; import { exportRankTrackingCsv, exportRankTrackingToSheets, } from "./RankTrackingTableParts"; import type { RankTrackingConfig, ComparePeriod, } from "@/types/schemas/rank-tracking"; import { AddKeywordsPanel } from "./AddKeywordsPanel"; import { FilterPanel, applyFilters, countActiveFilters, EMPTY_FILTERS, type Filters, } from "./RankTrackingFilters"; import { CheckConfirmModal } from "./CheckConfirmModal"; import { useMetricsRefresh } from "./useMetricsRefresh"; import { useRankCheckTrigger } from "./useRankCheckTrigger"; import { useRankRunPolling } from "./useRankRunPolling"; function deviceVisibility( devices: RankTrackingConfig["devices"], activeDevice: "desktop" | "mobile", ): { showDesktop: boolean; showMobile: boolean } { if (devices === "both") { return { showDesktop: activeDevice === "desktop", showMobile: activeDevice === "mobile", }; } return { showDesktop: devices !== "mobile", showMobile: devices !== "desktop", }; } export function RankTrackingDomainDetail(props: { config: RankTrackingConfig; projectId: string; onBack: () => void; onEdit: () => void; }) { return ( ); } function RankTrackingDomainDetailInner({ config, projectId, onBack, onEdit, }: { config: RankTrackingConfig; projectId: string; onBack: () => void; onEdit: () => void; }) { const { data: session } = useSession(); const customerQuery = useCustomer({ queryOptions: { enabled: Boolean(session?.user?.id) }, }); const isFreePlan = !!customerQuery.data && getCustomerPlanStatus(customerQuery.data) === "free"; const queryClient = useQueryClient(); const [showAddKeywords, setShowAddKeywords] = useState(false); const [showFilters, setShowFilters] = useState(false); const [filters, setFilters] = useState(EMPTY_FILTERS); const [comparePeriod, setComparePeriod] = useState( config.scheduleInterval === "daily" ? "1d" : "7d", ); const [activeDevice, setActiveDevice] = useState<"desktop" | "mobile">( config.devices === "mobile" ? "mobile" : "desktop", ); const [viewMode, setViewMode] = useState<"table" | "history">("table"); const { data: resultsData, isLoading: resultsLoading } = useQuery({ queryKey: ["rankTrackingResults", projectId, config.id, comparePeriod], queryFn: () => getLatestRankResults({ data: { projectId, configId: config.id, comparePeriod }, }), }); const latestRun = useRankRunPolling(projectId, config.id); // Also feeds the History toggle: the matrix view only earns its tab once // there are two checks to compare. const { data: matrixCells, isLoading: matrixLoading } = useQuery({ queryKey: ["rankPositionMatrix", projectId, config.id, activeDevice], queryFn: () => getRankPositionMatrix({ data: { projectId, configId: config.id, device: activeDevice }, }), }); const historyAvailable = countMatrixRuns(matrixCells ?? []) >= 2; const { data: costEstimate } = useQuery({ queryKey: ["rankTrackingCostEstimate", projectId, config.id], queryFn: () => estimateRankCheckCost({ data: { projectId, configId: config.id } }), }); const [pendingCheck, setPendingCheck] = useState<{ count: number; keywordIds?: string[]; } | null>(null); const handleKeywordsAdded = (result: { added: number; checkTriggered: boolean; }) => { void queryClient.invalidateQueries({ queryKey: ["rankTrackingCostEstimate", projectId, config.id], }); void queryClient.invalidateQueries({ queryKey: ["rankTrackingResults", projectId, config.id], }); void queryClient.invalidateQueries({ queryKey: ["rankTrackingLatestRun", projectId, config.id], }); setShowAddKeywords(false); captureClientEvent("rank_tracking:keywords_add"); toast.success( `${result.added} keyword${result.added !== 1 ? "s" : ""} added`, ); if (!result.checkTriggered && result.added > 0) { toast.info("Use 'Check Now' to check these keywords"); } }; const isRunning = (latestRun?.status === "pending" || latestRun?.status === "running") && !latestRun?.maybeStale; const { startCheck, isBusy, isPending } = useRankCheckTrigger({ configId: config.id, isRunning, projectId, onSuccess: () => setPendingCheck(null), }); const { refresh: refreshMetrics, isRefreshing: metricsRefreshing } = useMetricsRefresh(projectId, config.id); const requestCheck = (count: number, keywordIds?: string[]) => { if (count < 50) { startCheck({ keywordIds }); return; } if (isBusy) return; setPendingCheck({ count, keywordIds }); }; const rows = resultsData?.rows; const run = resultsData?.run; const hasBothDevices = config.devices === "both"; const { showDesktop, showMobile } = deviceVisibility( config.devices, activeDevice, ); const filtered = useMemo( () => applyFilters(rows ?? [], filters), [rows, filters], ); const activeFilterCount = countActiveFilters(filters); const defaultSortId = showDesktop ? "desktopPosition" : "mobilePosition"; // Fall back to the table if history disappears (e.g. device switch). const effectiveViewMode = historyAvailable ? viewMode : "table"; return (
{config.lastSkipReason === "insufficient_credits" && (
Last scheduled check was skipped due to insufficient credits. Top up your balance to resume automatic tracking.
)} {latestRun?.maybeStale && (
This run may be unresponsive and will be cleaned up automatically.
)} {/* Results card */}
{/* Domain header */} setShowAddKeywords((c) => !c)} /> {showAddKeywords && (
setShowAddKeywords(false)} />
)} {/* Portfolio overview */} {(rows?.length ?? 0) > 0 && ( )} {/* Table toolbar */} setShowFilters((c) => !c)} activeFilterCount={activeFilterCount} isRunning={isRunning} latestRun={latestRun} keywordCount={filtered.length} viewMode={effectiveViewMode} onViewModeChange={setViewMode} historyAvailable={historyAvailable} onExport={() => exportRankTrackingCsv( filtered, showDesktop, showMobile, config.domain, ) } onExportToSheets={() => exportRankTrackingToSheets(filtered, showDesktop, showMobile) } onCopyKeywords={() => { void navigator.clipboard.writeText( filtered.map((r) => r.keyword).join("\n"), ); toast.success("Keywords copied to clipboard"); }} onCheckNow={() => { const count = costEstimate?.keywordCount ?? rows?.length ?? 0; if (count > 0) requestCheck(count); }} onRefreshMetrics={refreshMetrics} metricsRefreshing={metricsRefreshing} checkBusy={isBusy} checkDisabled={isFreePlan} hasData={filtered.length > 0} /> {/* Filters panel */} {showFilters && ( setFilters(EMPTY_FILTERS)} /> )} {/* Table */}
{effectiveViewMode === "history" ? ( ({ trackingKeywordId: r.trackingKeywordId, keyword: r.keyword, }))} /> ) : ( )}
{pendingCheck && ( startCheck({ keywordIds: pendingCheck.keywordIds, }) } onCancel={() => setPendingCheck(null)} /> )}
); }