import React, { useState } from "react"; import { ObjectInputProps, PatchEvent, set } from "sanity"; import { Select, Button, Stack } from "@sanity/ui"; import useProEnabled from "../../hooks/useProEnabled"; import ProGate from "./ProGate"; import { SCHEMA_TYPES, FIELDS_BY_TYPE } from "../../constants/schemaTypes"; import CalendarIcon from "../icons/CalendarIcon"; import TrashIcon from "../icons/TrashIcon"; /* eslint-disable @typescript-eslint/no-explicit-any */ const FIELD_INPUT: React.CSSProperties = { width: "100%", padding: "9px 12px", background: "var(--card-bg-color)", border: "1px solid var(--card-border-color)", borderRadius: 8, color: "var(--card-fg-color)", fontSize: 13, outline: "none", fontFamily: "inherit", boxSizing: "border-box", transition: "border-color 0.15s, box-shadow 0.15s", }; const isFullWidthField = (fieldName: string) => { return [ "name", "description", "reviewBody", "image", "thumbnailUrl", "url", "sameAs", "location", "license", "provider", "hiringOrganization", "headline", ].includes(fieldName); }; function buildJsonLd(schemaOrg: Record): Record | null { if (!schemaOrg.schemaType) return null; const ld: Record = { "@context": "https://schema.org", "@type": schemaOrg.schemaType, }; if (schemaOrg.schemaType === "FAQPage" && Array.isArray(schemaOrg.faqItems)) { ld.mainEntity = schemaOrg.faqItems.map((item: { question: string; answer: string }) => ({ "@type": "Question", name: item.question, acceptedAnswer: { "@type": "Answer", text: item.answer }, })); return ld; } const fields = FIELDS_BY_TYPE[schemaOrg.schemaType] || []; const allowedFieldNames = new Set(fields.map((f) => f.name)); fields.forEach((f) => { if (schemaOrg[f.name]) { ld[f.name] = schemaOrg[f.name]; } }); const typeAllowsRating = allowedFieldNames.has("ratingValue") || allowedFieldNames.has("ratingCount"); if (typeAllowsRating && (schemaOrg.ratingValue || schemaOrg.ratingCount)) { ld.aggregateRating = { "@type": "AggregateRating", ...(schemaOrg.ratingValue ? { ratingValue: schemaOrg.ratingValue } : {}), ...(schemaOrg.ratingCount ? { reviewCount: schemaOrg.ratingCount } : {}), }; delete ld.ratingValue; delete ld.ratingCount; } const typeAllowsPrice = allowedFieldNames.has("price") || allowedFieldNames.has("priceCurrency") || allowedFieldNames.has("availability"); if (typeAllowsPrice && (schemaOrg.price || schemaOrg.priceCurrency)) { ld.offers = { "@type": "Offer", ...(schemaOrg.price ? { price: schemaOrg.price } : {}), ...(schemaOrg.priceCurrency ? { priceCurrency: schemaOrg.priceCurrency } : {}), ...(schemaOrg.availability ? { availability: `https://schema.org/${schemaOrg.availability}` } : {}), }; delete ld.price; delete ld.priceCurrency; delete ld.availability; } const typeAllowsAuthor = allowedFieldNames.has("author"); if (typeAllowsAuthor && schemaOrg.author) { ld.author = { "@type": "Person", name: schemaOrg.author }; } Object.keys(ld).forEach((key) => { if (key !== "@context" && key !== "@type") { if (!ld[key] || (typeof ld[key] === "string" && ld[key].trim() === "")) { delete ld[key]; } } }); return ld; } export default function SchemaWizardFieldInput({ value, onChange }: ObjectInputProps) { const { isPro } = useProEnabled(); const schemaOrg = (value as Record) || {}; const originalType = schemaOrg.schemaType || ""; const [showPreview, setShowPreview] = useState(false); const [selectedType, setSelectedType] = useState(originalType); const [faqItems, setFaqItems] = useState<{ question: string; answer: string }[]>( schemaOrg.faqItems || [{ question: "", answer: "" }], ); const [focusedField, setFocusedField] = useState(null); const fields = FIELDS_BY_TYPE[selectedType] || []; const filledCount = fields.filter((f) => schemaOrg[f.name]).length; const totalCount = fields.length; const patch = (fieldName: string, fieldValue: any) => { onChange(PatchEvent.from(set({ ...schemaOrg, [fieldName]: fieldValue }))); }; const handleTypeChange = (e: React.ChangeEvent) => { const newType = e.target.value; setSelectedType(newType); if (newType !== originalType) { onChange(PatchEvent.from(set({ ...schemaOrg, schemaType: newType }))); } if (newType !== "FAQPage") { setFaqItems([{ question: "", answer: "" }]); } else if (!Array.isArray(schemaOrg.faqItems)) { setFaqItems([{ question: "", answer: "" }]); } }; const addFaq = () => setFaqItems((prev) => [...prev, { question: "", answer: "" }]); const removeFaq = (i: number) => { const updated = faqItems.filter((_, idx) => idx !== i); const final = updated.length > 0 ? updated : [{ question: "", answer: "" }]; setFaqItems(final); patch("faqItems", final); }; const updateFaq = (i: number, field: "question" | "answer", val: string) => { const updated = faqItems.map((item, idx) => (idx === i ? { ...item, [field]: val } : item)); setFaqItems(updated); patch("faqItems", updated); }; const getFieldStyle = (fieldName: string) => { const isFocused = focusedField === fieldName; return { ...FIELD_INPUT, borderColor: isFocused ? "var(--card-link-color)" : "var(--card-border-color)", boxShadow: isFocused ? "0 0 0 2px rgba(29, 78, 216, 0.15)" : "none", }; }; const jsonLd = buildJsonLd(schemaOrg); const jsonLdStr = jsonLd ? JSON.stringify(jsonLd, null, 2) : null; return (
Structured Data (JSON-LD)
30+ schema types supported · Google Rich Results eligible
{selectedType && totalCount > 0 && (
{filledCount}/{totalCount} fields
)}
Schema Type
{selectedType === "FAQPage" && (
FAQ Items
{faqItems.map((item, i) => ( // eslint-disable-next-line react/no-array-index-key
{faqItems.length > 1 && ( )}
Question {i + 1}
updateFaq(i, "question", e.target.value)} placeholder="What is…?" onFocus={() => setFocusedField(`faq-q-${i}`)} onBlur={() => setFocusedField(null)} style={getFieldStyle(`faq-q-${i}`)} />
Answer