472 lines
12 KiB
TypeScript
472 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { cn } from "@/lib/utils";
|
|
import { ChevronRightIcon } from "lucide-react";
|
|
import type { ComponentProps, HTMLAttributes } from "react";
|
|
import { createContext, useContext, useMemo } from "react";
|
|
|
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
|
|
interface SchemaParameter {
|
|
name: string;
|
|
type: string;
|
|
required?: boolean;
|
|
description?: string;
|
|
location?: "path" | "query" | "header";
|
|
}
|
|
|
|
interface SchemaProperty {
|
|
name: string;
|
|
type: string;
|
|
required?: boolean;
|
|
description?: string;
|
|
properties?: SchemaProperty[];
|
|
items?: SchemaProperty;
|
|
}
|
|
|
|
interface SchemaDisplayContextType {
|
|
method: HttpMethod;
|
|
path: string;
|
|
description?: string;
|
|
parameters?: SchemaParameter[];
|
|
requestBody?: SchemaProperty[];
|
|
responseBody?: SchemaProperty[];
|
|
}
|
|
|
|
const SchemaDisplayContext = createContext<SchemaDisplayContextType>({
|
|
method: "GET",
|
|
path: "",
|
|
});
|
|
|
|
const methodStyles: Record<HttpMethod, string> = {
|
|
DELETE: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
|
GET: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
|
|
PATCH:
|
|
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
|
|
POST: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
|
PUT: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
|
|
};
|
|
|
|
export type SchemaDisplayHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const SchemaDisplayHeader = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayHeaderProps) => (
|
|
<div
|
|
className={cn("flex items-center gap-3 border-b px-4 py-3", className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export type SchemaDisplayMethodProps = ComponentProps<typeof Badge>;
|
|
|
|
export const SchemaDisplayMethod = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayMethodProps) => {
|
|
const { method } = useContext(SchemaDisplayContext);
|
|
|
|
return (
|
|
<Badge
|
|
className={cn("font-mono text-xs", methodStyles[method], className)}
|
|
variant="secondary"
|
|
{...props}
|
|
>
|
|
{children ?? method}
|
|
</Badge>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayPathProps = HTMLAttributes<HTMLSpanElement>;
|
|
|
|
export const SchemaDisplayPath = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayPathProps) => {
|
|
const { path } = useContext(SchemaDisplayContext);
|
|
|
|
// Highlight path parameters
|
|
const highlightedPath = path.replaceAll(
|
|
/\{([^}]+)\}/g,
|
|
'<span class="text-blue-600 dark:text-blue-400">{$1}</span>'
|
|
);
|
|
|
|
return (
|
|
<span
|
|
className={cn("font-mono text-sm", className)}
|
|
// oxlint-disable-next-line eslint-plugin-react(no-danger)
|
|
dangerouslySetInnerHTML={{ __html: children ?? highlightedPath }}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayDescriptionProps =
|
|
HTMLAttributes<HTMLParagraphElement>;
|
|
|
|
export const SchemaDisplayDescription = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayDescriptionProps) => {
|
|
const { description } = useContext(SchemaDisplayContext);
|
|
|
|
return (
|
|
<p
|
|
className={cn(
|
|
"border-b px-4 py-3 text-muted-foreground text-sm",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children ?? description}
|
|
</p>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayContentProps = HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const SchemaDisplayContent = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayContentProps) => (
|
|
<div className={cn("divide-y", className)} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export type SchemaDisplayParameterProps = HTMLAttributes<HTMLDivElement> &
|
|
SchemaParameter;
|
|
|
|
export const SchemaDisplayParameter = ({
|
|
name,
|
|
type,
|
|
required,
|
|
description,
|
|
location,
|
|
className,
|
|
...props
|
|
}: SchemaDisplayParameterProps) => (
|
|
<div className={cn("px-4 py-3 pl-10", className)} {...props}>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-sm">{name}</span>
|
|
<Badge className="text-xs" variant="outline">
|
|
{type}
|
|
</Badge>
|
|
{location && (
|
|
<Badge className="text-xs" variant="secondary">
|
|
{location}
|
|
</Badge>
|
|
)}
|
|
{required && (
|
|
<Badge
|
|
className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
|
variant="secondary"
|
|
>
|
|
required
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{description && (
|
|
<p className="mt-1 text-muted-foreground text-sm">{description}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
export type SchemaDisplayParametersProps = ComponentProps<typeof Collapsible>;
|
|
|
|
export const SchemaDisplayParameters = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayParametersProps) => {
|
|
const { parameters } = useContext(SchemaDisplayContext);
|
|
|
|
return (
|
|
<Collapsible className={cn(className)} defaultOpen {...props}>
|
|
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
|
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
<span className="font-medium text-sm">Parameters</span>
|
|
<Badge className="ml-auto text-xs" variant="secondary">
|
|
{parameters?.length}
|
|
</Badge>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="divide-y border-t">
|
|
{children ??
|
|
parameters?.map((param) => (
|
|
<SchemaDisplayParameter key={param.name} {...param} />
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayPropertyProps = HTMLAttributes<HTMLDivElement> &
|
|
SchemaProperty & {
|
|
depth?: number;
|
|
};
|
|
|
|
export const SchemaDisplayProperty = ({
|
|
name,
|
|
type,
|
|
required,
|
|
description,
|
|
properties,
|
|
items,
|
|
depth = 0,
|
|
className,
|
|
...props
|
|
}: SchemaDisplayPropertyProps) => {
|
|
const hasChildren = properties || items;
|
|
const paddingLeft = 40 + depth * 16;
|
|
|
|
if (hasChildren) {
|
|
return (
|
|
<Collapsible defaultOpen={depth < 2}>
|
|
<CollapsibleTrigger
|
|
className={cn(
|
|
"group flex w-full items-center gap-2 py-3 text-left transition-colors hover:bg-muted/50",
|
|
className
|
|
)}
|
|
style={{ paddingLeft }}
|
|
>
|
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
<span className="font-mono text-sm">{name}</span>
|
|
<Badge className="text-xs" variant="outline">
|
|
{type}
|
|
</Badge>
|
|
{required && (
|
|
<Badge
|
|
className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
|
variant="secondary"
|
|
>
|
|
required
|
|
</Badge>
|
|
)}
|
|
</CollapsibleTrigger>
|
|
{description && (
|
|
<p
|
|
className="pb-2 text-muted-foreground text-sm"
|
|
style={{ paddingLeft: paddingLeft + 24 }}
|
|
>
|
|
{description}
|
|
</p>
|
|
)}
|
|
<CollapsibleContent>
|
|
<div className="divide-y border-t">
|
|
{properties?.map((prop) => (
|
|
<SchemaDisplayProperty
|
|
key={prop.name}
|
|
{...prop}
|
|
depth={depth + 1}
|
|
/>
|
|
))}
|
|
{items && (
|
|
<SchemaDisplayProperty
|
|
{...items}
|
|
depth={depth + 1}
|
|
name={`${name}[]`}
|
|
/>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn("py-3 pr-4", className)}
|
|
style={{ paddingLeft }}
|
|
{...props}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{/* Spacer for alignment */}
|
|
<span className="size-4" />
|
|
<span className="font-mono text-sm">{name}</span>
|
|
<Badge className="text-xs" variant="outline">
|
|
{type}
|
|
</Badge>
|
|
{required && (
|
|
<Badge
|
|
className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
|
variant="secondary"
|
|
>
|
|
required
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{description && (
|
|
<p className="mt-1 pl-6 text-muted-foreground text-sm">{description}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayRequestProps = ComponentProps<typeof Collapsible>;
|
|
|
|
export const SchemaDisplayRequest = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayRequestProps) => {
|
|
const { requestBody } = useContext(SchemaDisplayContext);
|
|
|
|
return (
|
|
<Collapsible className={cn(className)} defaultOpen {...props}>
|
|
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
|
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
<span className="font-medium text-sm">Request Body</span>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="border-t">
|
|
{children ??
|
|
requestBody?.map((prop) => (
|
|
<SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayResponseProps = ComponentProps<typeof Collapsible>;
|
|
|
|
export const SchemaDisplayResponse = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayResponseProps) => {
|
|
const { responseBody } = useContext(SchemaDisplayContext);
|
|
|
|
return (
|
|
<Collapsible className={cn(className)} defaultOpen {...props}>
|
|
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
|
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
<span className="font-medium text-sm">Response</span>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="border-t">
|
|
{children ??
|
|
responseBody?.map((prop) => (
|
|
<SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayProps = HTMLAttributes<HTMLDivElement> & {
|
|
method: HttpMethod;
|
|
path: string;
|
|
description?: string;
|
|
parameters?: SchemaParameter[];
|
|
requestBody?: SchemaProperty[];
|
|
responseBody?: SchemaProperty[];
|
|
};
|
|
|
|
export const SchemaDisplay = ({
|
|
method,
|
|
path,
|
|
description,
|
|
parameters,
|
|
requestBody,
|
|
responseBody,
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayProps) => {
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
description,
|
|
method,
|
|
parameters,
|
|
path,
|
|
requestBody,
|
|
responseBody,
|
|
}),
|
|
[description, method, parameters, path, requestBody, responseBody]
|
|
);
|
|
|
|
return (
|
|
<SchemaDisplayContext.Provider value={contextValue}>
|
|
<div
|
|
className={cn(
|
|
"overflow-hidden rounded-lg border bg-background",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children ?? (
|
|
<>
|
|
<SchemaDisplayHeader>
|
|
<div className="flex items-center gap-3">
|
|
<SchemaDisplayMethod />
|
|
<SchemaDisplayPath />
|
|
</div>
|
|
</SchemaDisplayHeader>
|
|
{description && <SchemaDisplayDescription />}
|
|
<SchemaDisplayContent>
|
|
{parameters && parameters.length > 0 && (
|
|
<SchemaDisplayParameters />
|
|
)}
|
|
{requestBody && requestBody.length > 0 && (
|
|
<SchemaDisplayRequest />
|
|
)}
|
|
{responseBody && responseBody.length > 0 && (
|
|
<SchemaDisplayResponse />
|
|
)}
|
|
</SchemaDisplayContent>
|
|
</>
|
|
)}
|
|
</div>
|
|
</SchemaDisplayContext.Provider>
|
|
);
|
|
};
|
|
|
|
export type SchemaDisplayBodyProps = HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const SchemaDisplayBody = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayBodyProps) => (
|
|
<div className={cn("divide-y", className)} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export type SchemaDisplayExampleProps = HTMLAttributes<HTMLPreElement>;
|
|
|
|
export const SchemaDisplayExample = ({
|
|
className,
|
|
children,
|
|
...props
|
|
}: SchemaDisplayExampleProps) => (
|
|
<pre
|
|
className={cn(
|
|
"mx-4 mb-4 overflow-auto rounded-md bg-muted p-4 font-mono text-sm",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</pre>
|
|
);
|