import React, { useEffect, useState, useCallback } from "react";
import { useClient, useFormValue, PatchEvent, set } from "sanity";
import { Stack, Card, Flex, Text, Box, useTheme, Badge, Button } from "@sanity/ui";
import useProEnabled from "../../hooks/useProEnabled";
import ProGate from "./ProGate";
import CheckCircleIcon from "../icons/CheckCircleIcon";
import AlertCircleIcon from "../icons/AlertCircleIcon";
import ProgressRing from "../icons/ProgressRing";
/* eslint-disable @typescript-eslint/no-explicit-any */
interface CheckResult {
key: string;
label: string;
pass: boolean;
description?: string;
onFix?: () => void;
fixLabel?: string;
fixHint?: string;
}
const CheckRow = ({
check,
justFixed,
isDarkMode,
}: {
check: CheckResult;
justFixed: boolean;
isDarkMode: boolean;
}) => {
const passColor = "#10b981";
const hasDescription = !check.pass && !justFixed && (check.description || check.fixHint);
const showFixButton = !check.pass && !justFixed && check.onFix && check.fixLabel;
const hasSecondary = hasDescription || showFixButton;
let Icon = (
);
if (check.pass || justFixed) {
Icon = (
);
}
return (
{Icon}
{check.label}
{justFixed && (
Fixed!
)}
{!check.pass && !justFixed && check.description && (
{check.description}
)}
{!check.pass && !justFixed && check.fixHint && (
{check.fixHint}
)}
{!check.pass && !justFixed && check.onFix && check.fixLabel && (
)}
);
};
interface Props {
value: Record | undefined;
onChange: (e: any) => void;
}
export default function AdvancedValidation({ value, onChange }: Props) {
const { isPro } = useProEnabled();
const client = useClient({ apiVersion: "2024-01-01" });
const docId: string = (useFormValue(["_id"]) as string) || "";
const [checks, setChecks] = useState([]);
const [justFixed, setJustFixed] = useState>({});
const theme = useTheme();
const isDarkMode =
theme.sanity.v2?.color._dark ??
(theme.sanity as unknown as { color: { dark: boolean } }).color.dark;
const markFixed = useCallback((key: string) => {
setJustFixed((prev) => ({ ...prev, [key]: true }));
setTimeout(() => setJustFixed((prev) => ({ ...prev, [key]: false })), 2500);
}, []);
useEffect(() => {
let cancelled = false;
async function runChecks() {
const results: CheckResult[] = [];
const metaTitle: string = value?.metaTitle || "";
if (metaTitle) {
const dupId = await client.fetch(
`*[defined(seo.metaTitle) && seo.metaTitle == $title && _id != $id && !(_id in path("drafts.**"))][0]._id`,
{ title: metaTitle, id: docId },
);
results.push({
key: "dupTitle",
label: "Unique meta title",
pass: !dupId,
description: dupId
? "Another published document has the same meta title. Duplicate titles hurt rankings."
: undefined,
});
} else {
results.push({
key: "dupTitle",
label: "Unique meta title",
pass: false,
description: "No meta title set — set one in Basic SEO to check uniqueness.",
});
}
const hasOgImage: boolean = Boolean(value?.openGraph?.image?.asset);
results.push({
key: "ogImage",
label: "Open Graph image present",
pass: hasOgImage,
description: hasOgImage ? undefined : "No OG image — social shares will show no preview.",
fixHint: hasOgImage ? undefined : "→ Go to Social Sharing tab to add an OG image.",
});
const hasOgTitle: boolean = Boolean(value?.openGraph?.title);
const fixOgTitle = () => {
if (value?.metaTitle) {
onChange(PatchEvent.from(set(value.metaTitle, ["openGraph", "title"])));
}
};
results.push({
key: "ogTitle",
label: "Open Graph title set",
pass: hasOgTitle,
description: hasOgTitle
? undefined
: "OG title should be explicit — search engines may use a poor fallback.",
onFix: hasOgTitle ? undefined : fixOgTitle,
fixLabel: hasOgTitle ? undefined : "Copy from meta title",
});
const focusKeyword: string = (value?.focusKeyword || "").trim().toLowerCase();
const titleLower: string = metaTitle.toLowerCase();
const keywordInTitle =
Boolean(focusKeyword) && Boolean(metaTitle) && titleLower.includes(focusKeyword);
let focusKeywordDesc: string | undefined;
if (!keywordInTitle && !(!focusKeyword && !metaTitle)) {
focusKeywordDesc = focusKeyword
? `"${value?.focusKeyword}" not found in meta title — include it for better rankings.`
: "Set a focus keyword in Basic SEO to enable this check.";
}
results.push({
key: "focusKeyword",
label: "Focus keyword in meta title",
pass: keywordInTitle,
description: focusKeywordDesc,
});
const metaDesc: string = value?.metaDescription || "";
const descLen = metaDesc.length;
const goodDescLen = descLen >= 100 && descLen <= 160;
results.push({
key: "descLength",
label: "Meta description length (100–160 chars)",
pass: goodDescLen,
description: (() => {
if (!goodDescLen && metaDesc) {
const hint =
descLen < 100 ? "too short, add more context" : "too long, trim for full display";
return `Currently ${descLen} chars — ${hint}.`;
}
if (!goodDescLen) return "No meta description set — add one in Basic SEO.";
return undefined;
})(),
});
if (!cancelled) {
setChecks(results);
}
}
runChecks();
return () => {
cancelled = true;
};
}, [client, docId, value, onChange, markFixed]);
const passCount = checks.filter((c) => c.pass).length;
const issueCount = checks.filter((c) => !c.pass).length;
const allClear = checks.length > 0 && issueCount === 0;
const pct = checks.length > 0 ? Math.round((passCount / checks.length) * 100) : 0;
let barColor = "#ef4444";
if (allClear) barColor = "#10b981";
else if (pct >= 60) barColor = "#f59e0b";
let badgeTone: "positive" | "caution" | "critical" = "critical";
let badgeLabel = `${issueCount} issues`;
if (allClear) {
badgeTone = "positive";
badgeLabel = "All clear";
} else if (issueCount <= 2) {
badgeTone = "caution";
badgeLabel = `${issueCount} issue${issueCount !== 1 ? "s" : ""}`;
} else {
badgeLabel = `${issueCount} issue${issueCount !== 1 ? "s" : ""}`;
}
const fixableChecks = checks.filter((c) => !c.pass && c.onFix);
return (
{checks.length > 0 && (
)}
Advanced Validation
{checks.length > 0 && (
{badgeLabel}
)}
{fixableChecks.length > 0 && (
);
}