import React, { useState, useCallback, useEffect, useMemo } from "react"; import { useClient } from "sanity"; import { Box, Stack, Text, Spinner } from "@sanity/ui"; import { RefreshIcon, ActivityIcon, EditIcon, ClockIcon, CheckmarkCircleIcon } from "@sanity/icons"; import useProEnabled from "../../hooks/useProEnabled"; import { computeSEOScore } from "../../utils/seoScore"; import { scoreColor } from "./bulk/types"; import ProGate from "./ProGate"; import Pagination from "./Pagination"; import WorkflowStatCard from "./workflow/WorkflowStatCard"; import WorkflowRow from "./workflow/WorkflowRow"; import { WorkflowDoc, WorkflowStatus, STATUS_CFG, buildIssues } from "./workflow/types"; /* eslint-disable @typescript-eslint/no-explicit-any */ export default function WorkflowDashboard() { const client = useClient({ apiVersion: "2024-01-01" }); const { isPro } = useProEnabled(); const [docs, setDocs] = useState([]); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); const [filter, setFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all"); const [expandedId, setExpandedId] = useState(null); const [patching, setPatching] = useState>({}); const fetchDocs = useCallback(async () => { setLoading(true); try { const results: any[] = await client.fetch( `*[!(_id in path("drafts.**")) && defined(seo)] | order(_updatedAt desc) [0...300] { _id, _type, _updatedAt, "docTitle": coalesce(title, name, slug.current, "Untitled"), "seoStatus": coalesce(seo.seoStatus, "draft"), "reviewNotes": coalesce(seo.seoReviewNotes, ""), "seo": seo }`, { _ts: Date.now() }, ); setDocs( results.map((d) => ({ ...d, score: computeSEOScore(d.seo || undefined).score, issues: buildIssues(d.seo), reviewNotes: d.reviewNotes || "", })), ); setLoaded(true); } finally { setLoading(false); } }, [client]); useEffect(() => { fetchDocs(); }, [fetchDocs]); const patchStatus = useCallback( async (doc: WorkflowDoc, newStatus: WorkflowStatus) => { setPatching((p) => ({ ...p, [doc._id]: true })); try { await client.patch(doc._id).set({ "seo.seoStatus": newStatus }).commit(); setDocs((prev) => prev.map((d) => (d._id === doc._id ? { ...d, seoStatus: newStatus } : d)), ); } finally { setPatching((p) => ({ ...p, [doc._id]: false })); } }, [client], ); const patchNotes = useCallback( async (doc: WorkflowDoc, notes: string) => { await client.patch(doc._id).set({ "seo.seoReviewNotes": notes }).commit(); setDocs((prev) => prev.map((d) => (d._id === doc._id ? { ...d, reviewNotes: notes } : d))); }, [client], ); const draftCount = docs.filter((d) => d.seoStatus === "draft").length; const reviewCount = docs.filter((d) => d.seoStatus === "review").length; const approvedCount = docs.filter((d) => d.seoStatus === "approved").length; const avgScore = docs.length > 0 ? Math.round(docs.reduce((s, d) => s + d.score, 0) / docs.length) : 0; const docTypes = Array.from(new Set(docs.map((d) => d._type))).sort(); const visible = docs.filter((d) => { if (filter !== "all" && d.seoStatus !== filter) return false; if (typeFilter !== "all" && d._type !== typeFilter) return false; return true; }); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); useEffect(() => { setPage(0); }, [filter, typeFilter]); const paginatedDocs = useMemo(() => { return visible.slice(page * pageSize, (page + 1) * pageSize); }, [visible, page, pageSize]); return (
setFilter("all")} /> setFilter("draft")} /> setFilter("review")} /> setFilter("approved")} />
{loading && !loaded && (
Scanning documents…
)} {loaded && (
{visible.length === 0 && } {paginatedDocs.map((doc) => ( setExpandedId((prev) => (prev === doc._id ? null : doc._id))} patching={!!patching[doc._id]} onPatch={patchStatus} onSaveNotes={patchNotes} /> ))} {visible.length > 0 && (
{visible.length === docs.length ? `${docs.length} document${docs.length !== 1 ? "s" : ""}` : `${visible.length} of ${docs.length} documents`}
)}
)}
); } function Header({ loading, loaded, docCount, avgScore, onRefresh, }: { loading: boolean; loaded: boolean; docCount: number; avgScore: number; onRefresh: () => void; }) { const [refreshHovered, setRefreshHovered] = useState(false); return (
SEO Workflow
Workflow Dashboard
{loaded ? `${docCount} document${ docCount !== 1 ? "s" : "" } — avg score ${avgScore} — click a card to filter, expand a row to see issues` : "Loading your content workflow…"}
); } function FilterBar({ docs, filter, typeFilter, docTypes, visible, onFilterChange, onTypeFilterChange, }: { docs: WorkflowDoc[]; filter: WorkflowStatus | "all"; typeFilter: string; docTypes: string[]; visible: WorkflowDoc[]; onFilterChange: (f: WorkflowStatus | "all") => void; onTypeFilterChange: (t: string) => void; }) { return (
{(["all", "draft", "review", "approved"] as const).map((f) => { const isActive = filter === f; const cfg = f !== "all" ? STATUS_CFG[f] : null; const count = f === "all" ? docs.length : docs.filter((d) => d.seoStatus === f).length; return ( ); })}
{docTypes.length > 1 && (
Type:
)}
); } function EmptyState({ filter }: { filter: WorkflowStatus | "all" }) { const titles: Record = { review: "Nothing waiting for review", approved: "No approved documents yet", all: "No documents match this filter", draft: "No documents match this filter", }; const subs: Record = { review: "When a document is submitted for review it will appear here.", approved: "Mark a document as Approved to see it here.", all: "Try a different filter.", draft: "Try a different filter.", }; return (
{filter === "approved" ? "🎉" : "📭"}
{titles[filter] ?? "No documents match this filter"}
{subs[filter] ?? "Try a different filter."}
); }