import { useState } from "react"; import { toast } from "sonner"; import { useMutation } from "@tanstack/react-query"; import { createRankTrackingConfig, updateRankTrackingConfig, } from "@/serverFunctions/rank-tracking"; import { Info, Loader2, X } from "lucide-react"; import { Modal } from "@/client/components/Modal"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { captureClientEvent } from "@/client/lib/posthog"; import type { RankTrackingConfig } from "@/types/schemas/rank-tracking"; import { normalizeDomain } from "@/types/schemas/domain"; import { depthToPages, pagesToDepth, estimateRankCheckCredits, } from "@/shared/rank-tracking"; import { LOCATION_OPTIONS, DEFAULT_LOCATION_CODE, getLanguageCode, } from "@/client/features/keywords/locations"; import { KeywordSuggestionStep } from "./KeywordSuggestionStep"; type Props = { projectId: string; existingConfig?: RankTrackingConfig | null; onClose: () => void; onSaved: (createdConfigId?: string) => void; onConfigCreated?: () => void; }; export function RankTrackingConfigModal({ projectId, existingConfig, onClose, onSaved, onConfigCreated, }: Props) { const isEdit = !!existingConfig; const [step, setStep] = useState<"config" | "keywords">("config"); const [domain, setDomain] = useState(existingConfig?.domain ?? ""); const [devices, setDevices] = useState<"both" | "desktop" | "mobile">( existingConfig?.devices ?? "mobile", ); const [locationCode, setLocationCode] = useState( existingConfig?.locationCode ?? DEFAULT_LOCATION_CODE, ); const [serpDepth, setSerpDepth] = useState(existingConfig?.serpDepth ?? 40); const [schedule, setSchedule] = useState<"daily" | "weekly" | "manual">( existingConfig?.scheduleInterval ?? "weekly", ); const [createdConfigId, setCreatedConfigId] = useState(null); const createMutation = useMutation({ mutationFn: () => createRankTrackingConfig({ data: { projectId, domain, devices, serpDepth, locationCode, languageCode: getLanguageCode(locationCode), scheduleInterval: schedule, }, }), onSuccess: (result) => { captureClientEvent("rank_tracking:config_create"); toast.success("Domain added for rank tracking"); setCreatedConfigId(result.configId); onConfigCreated?.(); setStep("keywords"); }, onError: (error) => { toast.error(getStandardErrorMessage(error, "Failed to save config")); }, }); const updateMutation = useMutation({ mutationFn: () => updateRankTrackingConfig({ data: { projectId, configId: existingConfig!.id, domain, devices, serpDepth, locationCode, languageCode: getLanguageCode(locationCode), scheduleInterval: schedule, }, }), onSuccess: () => { captureClientEvent("rank_tracking:config_update"); toast.success("Configuration updated"); onSaved(); }, onError: (error) => { toast.error(getStandardErrorMessage(error, "Failed to update config")); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (isPending) return; if (!domain.trim()) { toast.error("Please enter a domain"); return; } if (isEdit) { updateMutation.mutate(); } else { createMutation.mutate(); } }; const handleDomainBlur = () => { try { setDomain(normalizeDomain(domain)); } catch { // Keep invalid partial input editable; submit validation will show the error. } }; const isPending = createMutation.isPending || updateMutation.isPending; if (step === "keywords" && createdConfigId) { return ( onSaved(id)} onClose={() => onSaved(createdConfigId ?? undefined)} /> ); } return (

{isEdit ? "Edit Domain Config" : "Add Domain"}

setDomain(e.target.value)} onBlur={handleDomainBlur} />
Most Google searches come from mobile, but select this based on your customer.
{devices === "both" && (
Tracking both devices uses 2x credits per keyword check
)}
{schedule === "daily" && (
Daily checks use 7x more credits than weekly
)}
10 pages is ~8x more expensive than 1 page
{(() => { const { costUsd: costPerKeyword } = estimateRankCheckCredits( 1, devices, serpDepth, ); const checksPerMonth = schedule === "daily" ? 30 : 4; return (
~${costPerKeyword.toFixed(4)} {" "} per keyword per check
{schedule !== "manual" && (
50 keywords would cost{" "} ~${(costPerKeyword * 50 * checksPerMonth).toFixed(2)} /month
)}
); })()}
); }