"use client";
import { DataType, Example, Required } from "@/components/fumastudio/badge";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { AllSchema, StringSchema } from "@trythis/js-schema";
import { Badge, Link2 } from "lucide-react";
import Link from "next/link";
import { createContext, type ReactNode, useContext, useState } from "react";
interface AccordionState {
openItems: Record<string, string>;
setOpenItem: (path: string, value: string) => void;
}
const SchemaContext = createContext<AccordionState | null>(null);
const SchemaProvider = (props: { children: ReactNode }) => {
const [openItems, setOpenItemsState] = useState<Record<string, string>>(
{},
);
const setOpenItem = (path: string, value: string) => {
setOpenItemsState((prev) => ({
...prev,
[path]: value,
}));
};
return (
<SchemaContext.Provider value={{ openItems, setOpenItem }}>
{props.children}
</SchemaContext.Provider>
);
};
const useSchema = () => {
const ctx = useContext(SchemaContext);
if (!ctx) throw new Error("useSchema must be used inside SchemaProvider");
return ctx;
};
type RenderType = "parameter" | "body" | "query" | "authorization" | "response";
type AuthSchemes = "basicAuth" | "bearerAuth" | "apiKeyHeader" | "apiKeyQuery";
type SchemaRendererProps = {
schema: AllSchema;
depth: number;
propName?: string;
isRequired?: boolean;
path: string;
type: RenderType;
};
function Schema(props: SchemaRendererProps) {
const { schema, depth, propName, isRequired, path, type } = props;
const { openItems, setOpenItem } = useSchema();
const isRootObject = depth === 0;
const isNestedObject = depth > 0;
const openValue = openItems[path] ?? "";
if (!schema) return <div>Unknown schema</div>;
// Primitives
if (
typeof schema.type === "string" &&
["string", "number"].includes(schema.type)
) {
const _schema = schema as StringSchema;
return (
<div className="primitive-param-field border-gray-100 dark:border-gray-800 border-b last:border-b-0">
<div className={depth === 1 ? "py-6" : "py-4"}>
<div className="flex font-mono text-sm break-all relative group/param-head param-head">
<div className="flex-1 flex flex-col content-start py-0.5 mr-5">
<div className="flex items-center flex-wrap gap-2">
<div className="absolute -top-1.5">
<Link
href={`#${type}-${propName}`}
className="-ml-10 flex items-center opacity-0 border-0 group-hover/param-head:opacity-100 focus:opacity-100 focus:outline-0 py-2 [.expandable-content_&]:-ml-[2.1rem] group/link">
<div className="w-6 h-6 rounded-md flex items-center justify-center shadow-sm text-gray-400 dark:text-white/50 dark:bg-background-dark dark:brightness-[1.35] dark:ring-1 dark:hover:brightness-150 bg-white ring-1 ring-gray-400/30 dark:ring-gray-700/25 hover:ring-gray-400/60 dark:hover:ring-white/20">
<Link2
className="h-4"
color="gray"
/>
</div>
</Link>
</div>
<div
id={`${type}-${propName}`}
className="font-semibold cursor-pointer text-primary dark:text-primary-light overflow-wrap-anywhere">
{propName}
</div>
<DataType type={_schema.type} />
{isRequired && <Required />}
</div>
</div>
</div>
<div className="mt-4">
{_schema.description && (
<div className="prose-sm prose prose-gray dark:prose-invert">
<p className="whitespace-pre-line">
{_schema.description}
</p>
</div>
)}
{_schema.example && (
<div className="flex prose prose-sm prose-gray dark:prose-invert mt-6 gap-1.5">
<span>Example:</span>
<Example
val={_schema.example}
/>
</div>
)}
{_schema.enum && (
<div className="flex prose prose-sm prose-gray dark:prose-invert mt-6 gap-1.5">
<span>Available options:</span>
{_schema.enum.map((item) => (
<DataType
key={item}
type={item}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}
// For nested objects
if (schema.type === "object" && isNestedObject) {
const entries = Object.entries(schema.properties ?? {});
return (
<div className="border-gray-100 dark:border-gray-800 border-b last:border-b-0 py-2">
<div className="flex font-mono text-sm break-all relative mb-4 group/param-head param-head">
<div className="flex-1 flex flex-col content-start py-0.5 mr-5">
<div className="flex items-center flex-wrap gap-2">
<div className="absolute -top-1.5">
<Link
href={`#${type}-${propName}`}
className="-ml-10 flex items-center opacity-0 border-0 group-hover/param-head:opacity-100 focus:opacity-100 focus:outline-0 py-2 [.expandable-content_&]:-ml-[2.1rem] group/link">
<div className="w-6 h-6 rounded-md flex items-center justify-center shadow-sm text-gray-400 dark:text-white/50 dark:bg-background-dark dark:brightness-[1.35] dark:ring-1 dark:hover:brightness-150 bg-white ring-1 ring-gray-400/30 dark:ring-gray-700/25 hover:ring-gray-400/60 dark:hover:ring-white/20 group-focus/link:border-2 group-focus/link:border-primary dark:group-focus/link:border-primary-light">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="gray"
height="12px"
viewBox="0 0 576 512">
<path d="M0 256C0 167.6 71.6 96 160 96h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C98.1 144 48 194.1 48 256s50.1 112 112 112h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C71.6 416 0 344.4 0 256zm576 0c0 88.4-71.6 160-160 160H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c61.9 0 112-50.1 112-112s-50.1-112-112-112H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c88.4 0 160 71.6 160 160zM184 232H392c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24s10.7-24 24-24z"></path>
</svg>
</div>
</Link>
</div>
<div
id={`${type}-${propName}`}
className="text-primary dark:text-primary-light cursor-pointer">
{propName}
</div>
<Badge type={schema.type} />
</div>
</div>
</div>
{entries.length > 0 && (
<Accordion
className="w-full"
collapsible
type="single"
value={openValue}
onValueChange={(val) =>
setOpenItem(path, val)
}>
<AccordionItem
className="last:border-b relative rounded-lg border bg-background px-4 py-0.5 outline-none has-focus-visible:z-10 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50"
value="item">
<AccordionTrigger className="[&>svg]:-order-1 justify-start gap-3 rounded-md py-1.5 text-[15px] leading-6 outline-none hover:no-underline focus-visible:ring-0 font-geist font-normal text-sm">
{openValue
? "Hide child attributes"
: "Show child attributes"}
</AccordionTrigger>
<AccordionContent>
{entries.map(
([
childName,
childSchema,
]) => (
<Schema
key={
childName
}
schema={
childSchema as AllSchema
}
propName={
childName
}
depth={
depth +
1
}
isRequired={schema.required?.includes(
childName,
)}
path={`${path}.${childName}`}
type={type}
/>
),
)}
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</div>
);
}
// Top level object should not be wrapped in Accordion
if (schema.type === "object" && isRootObject) {
const entries = Object.entries(schema.properties ?? {});
return (
<div className="border-gray-100 dark:border-gray-800 border-b last:border-b-0 py-2">
{propName && (
<div className="flex font-mono text-sm group/param-head param-head break-all relative">
<div className="flex-1 flex flex-col content-start py-0.5 mr-5">
<div className="flex items-center flex-wrap gap-2">
<div className="absolute -top-1.5">
<Link
href={`#${type}-${propName}`}
className="-ml-10 flex items-center opacity-0 border-0 group-hover/param-head:opacity-100 focus:opacity-100 focus:outline-0 py-2 [.expandable-content_&]:-ml-[2.1rem] group/link">
<div className="w-6 h-6 rounded-md flex items-center justify-center shadow-sm text-gray-400 dark:text-white/50 dark:bg-background-dark dark:brightness-[1.35] dark:ring-1 dark:hover:brightness-150 bg-white ring-1 ring-gray-400/30 dark:ring-gray-700/25 hover:ring-gray-400/60 dark:hover:ring-white/20 group-focus/link:border-2 group-focus/link:border-primary dark:group-focus/link:border-primary-light">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="gray"
height="12px"
viewBox="0 0 576 512">
<path d="M0 256C0 167.6 71.6 96 160 96h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C98.1 144 48 194.1 48 256s50.1 112 112 112h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C71.6 416 0 344.4 0 256zm576 0c0 88.4-71.6 160-160 160H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c61.9 0 112-50.1 112-112s-50.1-112-112-112H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c88.4 0 160 71.6 160 160zM184 232H392c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24s10.7-24 24-24z"></path>
</svg>
</div>
</Link>
</div>
<div
id={`${type}-${propName}`}
className="font-semibold text-primary dark:text-primary-light cursor-pointer">
{propName}
</div>
<Badge type={schema.type} />
</div>
</div>
</div>
)}
{entries.length > 0 && (
<>
{entries.map(([childName, childSchema]) => (
<Schema
key={childName}
schema={
childSchema as AllSchema
}
propName={childName}
depth={depth + 1}
isRequired={schema.required?.includes(
childName,
)}
path={`${path}.${childName}`}
type={type}
/>
))}
</>
)}
</div>
);
}
// Response bodies
if (schema.type === "json" && isRootObject) {
const entries = Object.entries(schema.properties ?? {});
return (
<div className="border-gray-100 dark:border-gray-800 border-b last:border-b-0 py-2">
{propName && (
<div className="flex font-mono text-sm group/param-head param-head break-all relative">
<div className="flex-1 flex flex-col content-start py-0.5 mr-5">
<div className="flex items-center flex-wrap gap-2">
<div className="absolute -top-1.5">
<Link
href={`#${type}-${propName}`}
className="-ml-10 flex items-center opacity-0 border-0 group-hover/param-head:opacity-100 focus:opacity-100 focus:outline-0 py-2 [.expandable-content_&]:-ml-[2.1rem] group/link">
<div className="w-6 h-6 rounded-md flex items-center justify-center shadow-sm text-gray-400 dark:text-white/50 dark:bg-background-dark dark:brightness-[1.35] dark:ring-1 dark:hover:brightness-150 bg-white ring-1 ring-gray-400/30 dark:ring-gray-700/25 hover:ring-gray-400/60 dark:hover:ring-white/20 group-focus/link:border-2 group-focus/link:border-primary dark:group-focus/link:border-primary-light">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="gray"
height="12px"
viewBox="0 0 576 512">
<path d="M0 256C0 167.6 71.6 96 160 96h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C98.1 144 48 194.1 48 256s50.1 112 112 112h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C71.6 416 0 344.4 0 256zm576 0c0 88.4-71.6 160-160 160H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c61.9 0 112-50.1 112-112s-50.1-112-112-112H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c88.4 0 160 71.6 160 160zM184 232H392c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24s10.7-24 24-24z"></path>
</svg>
</div>
</Link>
</div>
<div
id={`${type}-${propName}`}
className="font-semibold text-primary dark:text-primary-light cursor-pointer">
{propName}
</div>
<Badge type={schema.type} />
</div>
</div>
</div>
)}
{entries.length > 0 && (
<>
{entries.map(([childName, childSchema]) => (
<Schema
key={childName}
schema={
childSchema as AllSchema
}
propName={childName}
depth={depth + 1}
isRequired={schema.required?.includes(
childName,
)}
path={`${path}.${childName}`}
type={type}
/>
))}
</>
)}
</div>
);
}
if (
typeof schema.type === "string" &&
(
[
"basicAuth",
"bearerAuth",
"apiKeyHeader",
"apiKeyQuery",
] as AuthSchemes[]
).includes(schema.type as AuthSchemes)
) {
const _schema = schema as StringSchema;
return (
<div className="primitive-param-field border-gray-100 dark:border-gray-800 border-b last:border-b-0">
<div className={depth === 1 ? "py-6" : "py-4"}>
<div className="flex font-mono text-sm break-all relative group/param-head param-head">
<div className="flex-1 flex flex-col content-start py-0.5 mr-5">
<div className="flex items-center flex-wrap gap-2">
<div className="absolute -top-1.5">
<Link
href={`#authorization-authorization`}
className="-ml-10 flex items-center opacity-0 border-0 group-hover/param-head:opacity-100 focus:opacity-100 focus:outline-0 py-2 [.expandable-content_&]:-ml-[2.1rem] group/link">
<div className="w-6 h-6 rounded-md flex items-center justify-center shadow-sm text-gray-400 dark:text-white/50 dark:bg-background-dark dark:brightness-[1.35] dark:ring-1 dark:hover:brightness-150 bg-white ring-1 ring-gray-400/30 dark:ring-gray-700/25 hover:ring-gray-400/60 dark:hover:ring-white/20">
<Link2
className="h-4"
color="gray"
/>
</div>
</Link>
</div>
<div
id={`${type}-${propName}`}
className="font-semibold cursor-pointer text-primary dark:text-primary-light overflow-wrap-anywhere">
Authorization
</div>
{/* <DataType type={_schema.type} /> */}
<div className="flex items-center gap-2 text-xs font-medium [&_div]:inline [&_div]:mr-2 [&_div]:leading-5">
<div className="flex items-center px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-gray-600 dark:text-gray-200 font-medium break-all">
<span>string</span>
</div>
<div className="flex items-center px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-gray-600 dark:text-gray-200 font-medium break-all">
<span>header</span>
</div>
<Required />
</div>
</div>
</div>
</div>
<div className="mt-4">
{_schema.description && (
<div className="prose-sm prose prose-gray dark:prose-invert">
<p className="whitespace-pre-line">
{_schema.description}
</p>
</div>
)}
{_schema.example && (
<div className="flex prose prose-sm prose-gray dark:prose-invert mt-6 gap-1.5">
<span>Example:</span>
<Example
val={_schema.example}
/>
</div>
)}
{_schema.enum && (
<div className="flex prose prose-sm prose-gray dark:prose-invert mt-6 gap-1.5">
<span>Available options:</span>
{_schema.enum.map((item) => (
<DataType
key={item}
type={item}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}
// Unsupported schema
return <></>;
}
export { Schema, SchemaProvider, useSchema };