mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 07:18:56 -05:00
164 lines
3.9 KiB
TypeScript
164 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import type {
|
|
SearchItemType,
|
|
SharedProps,
|
|
} from "fumadocs-ui/components/dialog/search";
|
|
import {
|
|
SearchDialog,
|
|
SearchDialogClose,
|
|
SearchDialogContent,
|
|
SearchDialogFooter,
|
|
SearchDialogHeader,
|
|
SearchDialogInput,
|
|
SearchDialogList,
|
|
SearchDialogListItem,
|
|
SearchDialogOverlay,
|
|
useSearch,
|
|
} from "fumadocs-ui/components/dialog/search";
|
|
import { ArrowRight, Search } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useMemo } from "react";
|
|
import { Client } from "typesense";
|
|
import { useTypesenseSearch } from "typesense-fumadocs-adapter/client";
|
|
import { usePages } from "@/app/docs/provider";
|
|
|
|
const typesenseClient = (() => {
|
|
const url = process.env.NEXT_PUBLIC_TYPESENSE_SERVER_URL;
|
|
const apiKey = process.env.NEXT_PUBLIC_TYPESENSE_SEARCH_API_KEY;
|
|
if (!url || !apiKey) {
|
|
// Return a dummy client so the app works without Typesense configured
|
|
return new Client({
|
|
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
|
|
apiKey: "dummy",
|
|
});
|
|
}
|
|
let serverUrl: URL;
|
|
try {
|
|
serverUrl = new URL(url);
|
|
} catch {
|
|
return new Client({
|
|
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
|
|
apiKey: "dummy",
|
|
});
|
|
}
|
|
return new Client({
|
|
nodes: [
|
|
{
|
|
host: serverUrl.hostname,
|
|
port:
|
|
Number(serverUrl.port) ||
|
|
(serverUrl.protocol === "https:" ? 443 : 80),
|
|
protocol: serverUrl.protocol.replace(":", ""),
|
|
},
|
|
],
|
|
apiKey,
|
|
});
|
|
})();
|
|
|
|
export default function CustomSearchDialog(props: SharedProps) {
|
|
const { search, setSearch, query } = useTypesenseSearch({
|
|
typesenseCollectionName: "better-auth-docs",
|
|
client: typesenseClient!,
|
|
/**
|
|
* Non-legacy mode leaves raw <mark> tags in content at the moment,
|
|
* which renders as plain text in fumadocs-ui
|
|
*/
|
|
legacy: true,
|
|
});
|
|
const pages = usePages();
|
|
const router = useRouter();
|
|
|
|
const pageTreeAction = useMemo<SearchItemType | undefined>(() => {
|
|
if (search.length === 0) return;
|
|
|
|
const normalized = search.toLowerCase();
|
|
for (const page of pages) {
|
|
if (!page.name.toLowerCase().includes(normalized)) continue;
|
|
|
|
return {
|
|
id: "quick-action",
|
|
type: "action",
|
|
node: (
|
|
<div className="inline-flex items-center gap-2 text-fd-muted-foreground">
|
|
<ArrowRight className="size-4" />
|
|
<p>
|
|
Jump to{" "}
|
|
<span className="font-medium text-fd-foreground">
|
|
{page.name}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
),
|
|
onSelect: () => router.push(page.url),
|
|
};
|
|
}
|
|
}, [router, search, pages]);
|
|
|
|
return (
|
|
<SearchDialog
|
|
search={search}
|
|
onSearchChange={setSearch}
|
|
isLoading={query.isLoading}
|
|
{...props}
|
|
>
|
|
<SearchDialogOverlay className="z-200" />
|
|
<SearchDialogContent className="z-200">
|
|
<SearchDialogHeader>
|
|
<LoadingSearchIcon />
|
|
<SearchDialogInput />
|
|
<SearchDialogClose />
|
|
</SearchDialogHeader>
|
|
<SearchDialogList
|
|
items={
|
|
query.data !== "empty" || pageTreeAction
|
|
? [
|
|
...(pageTreeAction ? [pageTreeAction] : []),
|
|
...(Array.isArray(query.data) ? query.data : []),
|
|
]
|
|
: null
|
|
}
|
|
Item={({ item, onClick }) => (
|
|
<SearchDialogListItem
|
|
item={item}
|
|
onClick={onClick}
|
|
className={
|
|
item.type !== "action"
|
|
? "max-h-24 [&>div:last-child]:line-clamp-2"
|
|
: undefined
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
<SearchDialogFooter>
|
|
<span className="text-xs text-fd-muted-foreground">
|
|
Search powered by{" "}
|
|
<a
|
|
href="https://typesense.org"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="underline hover:text-fd-foreground transition-colors"
|
|
>
|
|
Typesense
|
|
</a>
|
|
</span>
|
|
</SearchDialogFooter>
|
|
</SearchDialogContent>
|
|
</SearchDialog>
|
|
);
|
|
}
|
|
|
|
function LoadingSearchIcon() {
|
|
const { isLoading } = useSearch();
|
|
|
|
return (
|
|
<Search
|
|
className={
|
|
isLoading
|
|
? "size-5 animate-pulse text-foreground duration-400"
|
|
: "size-5 text-fd-muted-foreground"
|
|
}
|
|
/>
|
|
);
|
|
}
|