mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
353 lines
9.6 KiB
TypeScript
353 lines
9.6 KiB
TypeScript
import { Link } from "lucide-react";
|
|
import type { JSX, ReactNode } from "react";
|
|
import { Children, isValidElement } from "react";
|
|
import type { ApiMethodOptions, ApiMethodProperty } from "@/lib/api-method";
|
|
import { generateApiMethodExamples, parseApiMethod } from "@/lib/api-method";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
ApiMethodTabs,
|
|
ApiMethodTabsContent,
|
|
ApiMethodTabsList,
|
|
ApiMethodTabsTrigger,
|
|
} from "./api-method-tabs";
|
|
import { Endpoint } from "./endpoint";
|
|
import { Button } from "./ui/button";
|
|
import { DynamicCodeBlock } from "./ui/dynamic-code-block";
|
|
|
|
const apiMethodTabs = [
|
|
{
|
|
value: "client",
|
|
label: "Client",
|
|
icon: (
|
|
<>
|
|
<rect width="20" height="14" x="2" y="3" rx="2" />
|
|
<path d="M8 21h8m-4-4v4" />
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
value: "server",
|
|
label: "Server",
|
|
icon: (
|
|
<>
|
|
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
|
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
|
<path d="M6 6h.01M6 18h.01" />
|
|
</>
|
|
),
|
|
},
|
|
] as const;
|
|
|
|
export function APIMethod({
|
|
children,
|
|
...options
|
|
}: ApiMethodOptions & { children: ReactNode }) {
|
|
const {
|
|
path,
|
|
method,
|
|
isServerOnly,
|
|
isClientOnly,
|
|
isExternalOnly,
|
|
note,
|
|
clientOnlyNote,
|
|
serverOnlyNote,
|
|
} = options;
|
|
const definition = parseApiMethod(getTextContent(children));
|
|
const examples = generateApiMethodExamples(definition, options);
|
|
const properties = definition.properties;
|
|
|
|
const serverTabContent = (
|
|
<div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
|
|
{isClientOnly || isServerOnly ? null : (
|
|
<Endpoint method={method ?? "GET"} path={path} />
|
|
)}
|
|
{serverOnlyNote || note ? (
|
|
<Note>
|
|
{note && tsxifyBackticks(note)}
|
|
{serverOnlyNote ? (
|
|
<>
|
|
{note ? <br /> : null}
|
|
{tsxifyBackticks(serverOnlyNote)}
|
|
</>
|
|
) : null}
|
|
</Note>
|
|
) : null}
|
|
<div className="relative w-full">
|
|
<DynamicCodeBlock
|
|
code={examples.server}
|
|
lang="ts"
|
|
allowCopy={!isClientOnly}
|
|
/>
|
|
{isClientOnly ? (
|
|
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
|
|
<span>This is a client-only endpoint</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{!isClientOnly ? <TypeTable properties={properties} isServer /> : null}
|
|
</div>
|
|
);
|
|
|
|
if (isExternalOnly) {
|
|
return serverTabContent;
|
|
}
|
|
|
|
const pathId = path.replaceAll("/", "-");
|
|
|
|
return (
|
|
<>
|
|
<div className="relative">
|
|
<div
|
|
id={`api-method${pathId}`}
|
|
aria-hidden
|
|
className="absolute invisible -top-[100px]"
|
|
/>
|
|
</div>
|
|
<ApiMethodTabs
|
|
defaultValue={isServerOnly ? "server" : "client"}
|
|
className="gap-0 w-full"
|
|
>
|
|
<ApiMethodTabsList className="relative flex justify-start w-full p-0 bg-background hover:[&>div>a>button]:opacity-100">
|
|
{apiMethodTabs.map((tab) => (
|
|
<ApiMethodTabsTrigger
|
|
key={tab.value}
|
|
value={tab.value}
|
|
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-fd-muted/80 hover:bg-fd-secondary/70 bg-background border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none dark:bg-[#050505] dark:hover:bg-[#0a0a0a] dark:data-[state=active]:bg-fd-muted/80 dark:border-white/[0.06] dark:hover:border-white/10 dark:data-[state=active]:border-white/10"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="1em"
|
|
height="1em"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
{tab.icon}
|
|
</svg>
|
|
<span>{tab.label}</span>
|
|
</ApiMethodTabsTrigger>
|
|
))}
|
|
<div className="absolute right-0">
|
|
<a href={`#api-method${pathId}`}>
|
|
<Button
|
|
variant="ghost"
|
|
className="opacity-100 transition-all duration-150 ease-in-out scale-90 md:opacity-0"
|
|
size="icon"
|
|
>
|
|
<Link className="size-4" />
|
|
</Button>
|
|
</a>
|
|
</div>
|
|
</ApiMethodTabsList>
|
|
<ApiMethodTabsContent value="client">
|
|
<div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
|
|
{isServerOnly ? null : (
|
|
<Endpoint method={method ?? "GET"} path={path} />
|
|
)}
|
|
{clientOnlyNote || note ? (
|
|
<Note>
|
|
{note && tsxifyBackticks(note)}
|
|
{clientOnlyNote ? (
|
|
<>
|
|
{note ? <br /> : null}
|
|
{tsxifyBackticks(clientOnlyNote)}
|
|
</>
|
|
) : null}
|
|
</Note>
|
|
) : null}
|
|
<div className="relative w-full">
|
|
<DynamicCodeBlock
|
|
code={examples.client}
|
|
lang="ts"
|
|
allowCopy={!isServerOnly}
|
|
/>
|
|
{isServerOnly ? (
|
|
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
|
|
<span>This is a server-only endpoint</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{!isServerOnly ? (
|
|
<TypeTable properties={properties} isServer={false} />
|
|
) : null}
|
|
</div>
|
|
</ApiMethodTabsContent>
|
|
<ApiMethodTabsContent value="server">
|
|
{serverTabContent}
|
|
</ApiMethodTabsContent>
|
|
</ApiMethodTabs>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function getTextContent(node: ReactNode): string {
|
|
return Children.toArray(node)
|
|
.map((child) =>
|
|
isValidElement<{ children?: ReactNode }>(child)
|
|
? getTextContent(child.props.children)
|
|
: String(child),
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function TypeTable({
|
|
properties,
|
|
isServer,
|
|
}: {
|
|
properties: ApiMethodProperty[];
|
|
isServer: boolean;
|
|
}) {
|
|
const visibleProperties = properties.filter(
|
|
(property) =>
|
|
!(property.serverOnly && isServer === false) &&
|
|
!(property.clientOnly && isServer === true),
|
|
);
|
|
|
|
if (!visibleProperties.length) return null;
|
|
|
|
return (
|
|
<div className="mt-0">
|
|
<div className="flex items-center gap-2 px-3.5 py-2 border-y border-border bg-fd-muted/80">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-muted-foreground"
|
|
>
|
|
<path d="M16 3h5v5" />
|
|
<path d="M8 3H3v5" />
|
|
<path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3" />
|
|
<path d="m15 9 6-6" />
|
|
</svg>
|
|
<span className="text-xs font-medium text-muted-foreground tracking-wider">
|
|
Parameters
|
|
</span>
|
|
</div>
|
|
<PropertyList properties={visibleProperties} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PropertyItem({ property }: { property: ApiMethodProperty }) {
|
|
return (
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<code className="text-xs font-semibold text-foreground/90">
|
|
{property.name}
|
|
</code>
|
|
<span className="text-xs font-mono text-foreground/60 font-medium">
|
|
{property.type}
|
|
{property.nullable ? " | null" : ""}
|
|
</span>
|
|
{!property.optional && (
|
|
<span className="text-[10px] font-mono font-medium text-amber-600 dark:text-amber-500/80">
|
|
required
|
|
</span>
|
|
)}
|
|
{property.serverOnly && (
|
|
<span className="text-[10px] font-mono font-medium text-blue-600 dark:text-blue-400/80">
|
|
server
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PropertyList({
|
|
properties,
|
|
nested = false,
|
|
}: {
|
|
properties: ApiMethodProperty[];
|
|
nested?: boolean;
|
|
}) {
|
|
const groups: {
|
|
property: ApiMethodProperty;
|
|
children: ApiMethodProperty[];
|
|
}[] = [];
|
|
let propertyIndex = 0;
|
|
|
|
while (propertyIndex < properties.length) {
|
|
const property = properties[propertyIndex];
|
|
if (property.type === "Object") {
|
|
const parentSegments = [...property.path, property.name];
|
|
const children: ApiMethodProperty[] = [];
|
|
propertyIndex++;
|
|
while (
|
|
propertyIndex < properties.length &&
|
|
properties[propertyIndex].path.length >= parentSegments.length &&
|
|
parentSegments.every(
|
|
(segment, index) => properties[propertyIndex].path[index] === segment,
|
|
)
|
|
) {
|
|
children.push(properties[propertyIndex]);
|
|
propertyIndex++;
|
|
}
|
|
groups.push({ property, children });
|
|
} else {
|
|
groups.push({ property, children: [] });
|
|
propertyIndex++;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="divide-y divide-border">
|
|
{groups.map((group) => (
|
|
<div
|
|
key={`${group.property.path.join(".")}.${group.property.name}`}
|
|
className={cn(
|
|
nested ? "px-3 py-3" : "px-3.5 py-3",
|
|
group.children.length > 0 && "pb-3",
|
|
)}
|
|
>
|
|
<PropertyItem property={group.property} />
|
|
{group.property.description && (
|
|
<p className="mt-1 mb-0 text-sm leading-relaxed max-w-xl">
|
|
{tsxifyBackticks(group.property.description)}
|
|
</p>
|
|
)}
|
|
{group.children.length > 0 && (
|
|
<div className="mt-3 border rounded-md overflow-hidden">
|
|
<PropertyList properties={group.children} nested />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function tsxifyBackticks(input: string): JSX.Element {
|
|
const parts = input.split(/(`[^`]+`)/g);
|
|
|
|
return (
|
|
<>
|
|
{parts.map((part, index) => {
|
|
if (part.startsWith("`") && part.endsWith("`")) {
|
|
const content = part.slice(1, -1);
|
|
return <code key={index}>{content}</code>;
|
|
}
|
|
return <span key={index}>{part}</span>;
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Note({ children }: { children: ReactNode }) {
|
|
return (
|
|
<div className="flex relative flex-col gap-2 p-3 mb-2 w-full wrap-break-word rounded-md border-b text-md text-wrap bg-fd-muted/80">
|
|
<span className="-mb-1 w-full text-xs select-none text-foreground/80 font-medium">
|
|
Notes
|
|
</span>
|
|
<p className="mt-0 mb-0 text-sm text-fd-muted-foreground">{children}</p>
|
|
</div>
|
|
);
|
|
}
|