Files
Metify/src/components/pretty-schema-display.tsx

204 lines
5.4 KiB
TypeScript

"use client";
import React from "react";
import { Badge } from "@/components/ui/badge";
import {
Link as LinkIcon,
Type,
Image as ImageIcon,
User,
Building,
Calendar,
HelpCircle,
Hash,
Text,
} from "lucide-react";
const keyMappings: { [key: string]: string } = {
"@type": "Type",
"@context": "Context",
"@id": "ID",
name: "Name",
headline: "Headline",
description: "Description",
author: "Author",
publisher: "Publisher",
mainEntityOfPage: "Main Page",
image: "Image",
datePublished: "Date Published",
dateModified: "Date Modified",
acceptedAnswer: "Answer",
mainEntity: "Main Content",
url: "URL",
text: "Text",
question: "Question",
answer: "Answer",
logo: "Logo",
telephone: "Telephone",
email: "Email",
};
const keyIcons: { [key: string]: React.ElementType } = {
"@type": Type,
"@id": Hash,
url: LinkIcon,
image: ImageIcon,
logo: ImageIcon,
author: User,
publisher: Building,
datePublished: Calendar,
dateModified: Calendar,
question: HelpCircle,
text: Text,
};
const renderValue = (value: any, level: number): React.ReactNode => {
if (typeof value === "string") {
if (value.startsWith("http://") || value.startsWith("https://")) {
try {
const url = new URL(value);
return (
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline break-all inline-flex items-center gap-1.5"
>
<LinkIcon className="h-3 w-3 flex-shrink-0" />
<span>
{url.hostname}
{url.pathname.length > 1 ? "/..." : ""}
</span>
</a>
);
} catch (e) {
return <span className="break-all">{value}</span>;
}
}
return <span className="break-words">{value}</span>;
}
if (typeof value === "object" && value !== null) {
if (Array.isArray(value)) {
return (
<div className="flex flex-col gap-1.5">
{value.map((item, index) => (
<React.Fragment key={index}>
{renderValue(item, level + 1)}
</React.Fragment>
))}
</div>
);
}
return (
<SchemaObjectRenderer data={value} isNested={true} level={level + 1} />
);
}
return <span>{String(value)}</span>;
};
const FaqQuestion = ({ item }: { item: any }) => {
if (
item["@type"] === "Question" &&
item.name &&
item.acceptedAnswer?.text
) {
return (
<div className="p-2 border rounded-md bg-background text-sm space-y-1">
<div className="grid grid-cols-[auto_1fr] gap-x-2 items-baseline">
<strong className="font-semibold text-muted-foreground">
Question:
</strong>
<p className="text-foreground">{item.name}</p>
</div>
<div className="grid grid-cols-[auto_1fr] gap-x-2 items-baseline">
<strong className="font-semibold text-muted-foreground">
Answer:
</strong>
<p className="text-foreground">{item.acceptedAnswer.text}</p>
</div>
</div>
);
}
// Fallback for non-standard Q&A items
return <SchemaObjectRenderer data={item} isNested={true} level={2} />;
};
const SchemaObjectRenderer = ({
data,
isNested = false,
level = 0,
}: {
data: any;
isNested?: boolean;
level?: number;
}) => {
const gridClasses =
level > 0 ? "md:grid-cols-[10rem_1fr]" : "md:grid-cols-[12rem_1fr]";
const paddingClasses = level > 1 ? "p-1.5" : "p-2";
const content = (
<div
className={`inline-grid grid-cols-1 ${gridClasses} items-start gap-x-4 gap-y-1.5`}
>
{Object.entries(data).map(([key, value]) => {
if (key === "@context") return null;
// Custom rendering for FAQPage questions
if (
key === "mainEntity" &&
data["@type"] === "FAQPage" &&
Array.isArray(value)
) {
return (
<React.Fragment key={key}>
<div className="flex items-center gap-2 font-semibold text-sm text-muted-foreground flex-shrink-0 whitespace-nowrap pt-0.5">
<span>Questions</span>
</div>
<div className="text-sm text-foreground space-y-2">
{value.map((item, index) => (
<FaqQuestion key={index} item={item} />
))}
</div>
</React.Fragment>
);
}
const label =
keyMappings[key] || key.charAt(0).toUpperCase() + key.slice(1);
const Icon = keyIcons[key];
return (
<React.Fragment key={key}>
<div className="flex items-center gap-2 font-semibold text-sm text-muted-foreground flex-shrink-0 whitespace-nowrap">
{Icon && <Icon className="h-4 w-4" />}
<span>{label}</span>
</div>
<div className="text-sm text-foreground">
{key === "@type" ? (
<Badge variant="secondary">{value as string}</Badge>
) : (
renderValue(value, level)
)}
</div>
</React.Fragment>
);
})}
</div>
);
if (isNested) {
return (
<div className={`${paddingClasses} border rounded-lg bg-muted/50`}>
{content}
</div>
);
}
return content;
};
export function PrettySchemaDisplay({ schema }: { schema: any }) {
return <SchemaObjectRenderer data={schema} />;
}