feat(website): include loading indicators when data is fetching

This commit is contained in:
iCrawl
2025-05-16 00:19:17 +02:00
parent 576443c29a
commit 33d8619a4e
6 changed files with 64 additions and 14 deletions

View File

@@ -12,7 +12,7 @@ export default async function Layout({ children }: PropsWithChildren) {
<> <>
<Sidebar closeButton={false} intent="inset"> <Sidebar closeButton={false} intent="inset">
<SidebarHeader /> <SidebarHeader />
<SidebarContent className="bg-[#f3f3f4] p-0 py-4 pl-4 dark:bg-[#121214]"> <SidebarContent className="bg-[#f3f3f4] p-0 pb-4 pl-4 dark:bg-[#121214]">
<Scrollbars> <Scrollbars>
<Navigation /> <Navigation />
</Scrollbars> </Scrollbars>

View File

@@ -1,10 +1,17 @@
'use client'; 'use client';
import { Loader2Icon } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { Select, SelectList, SelectOption, SelectTrigger } from '@/components/ui/Select';
import { parseDocsPathParams } from '@/util/parseDocsPathParams'; import { parseDocsPathParams } from '@/util/parseDocsPathParams';
import { Select, SelectList, SelectOption, SelectTrigger } from './ui/Select';
export function EntryPointSelect({ entryPoints }: { readonly entryPoints: { readonly entryPoint: string }[] }) { export function EntryPointSelect({
entryPoints,
isLoading,
}: {
readonly entryPoints: { readonly entryPoint: string }[];
readonly isLoading: boolean;
}) {
const router = useRouter(); const router = useRouter();
const params = useParams<{ const params = useParams<{
item?: string[] | undefined; item?: string[] | undefined;
@@ -16,11 +23,24 @@ export function EntryPointSelect({ entryPoints }: { readonly entryPoints: { read
return ( return (
<Select <Select
aria-label="Select an entrypoint" aria-label={isLoading ? 'Loading entrypoints...' : 'Select an entrypoint'}
defaultSelectedKey={parsedEntrypoints.join('/')} defaultSelectedKey={parsedEntrypoints.join('/')}
key={parsedEntrypoints.join('/')} key={parsedEntrypoints.join('/')}
placeholder={isLoading ? 'Loading entrypoints...' : 'Select an entrypoint'}
> >
<SelectTrigger className="bg-[#f3f3f4] dark:bg-[#121214]" /> <SelectTrigger
className="bg-[#f3f3f4] dark:bg-[#121214]"
suffix={
isLoading ? (
<Loader2Icon
aria-hidden
className="size-6 shrink-0 animate-spin duration-200 forced-colors:text-[ButtonText] forced-colors:group-disabled:text-[GrayText]"
size={24}
strokeWidth={1.5}
/>
) : null
}
/>
<SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={entryPoints}> <SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={entryPoints}>
{(item) => ( {(item) => (
<SelectOption <SelectOption

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { ChevronDown, ChevronUp } from 'lucide-react'; import { ChevronDown, ChevronUp, Loader2Icon } from 'lucide-react';
import { notFound, useParams } from 'next/navigation'; import { notFound, useParams } from 'next/navigation';
import { parseDocsPathParams } from '@/util/parseDocsPathParams'; import { parseDocsPathParams } from '@/util/parseDocsPathParams';
import { resolveNodeKind } from './DocKind'; import { resolveNodeKind } from './DocKind';
@@ -17,7 +17,11 @@ export function Navigation() {
const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item); const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item);
const { data: node, status } = useQuery({ const {
data: node,
status,
isLoading,
} = useQuery({
queryKey: ['sitemap', params.packageName, params.version, parsedEntrypoints.join('.')], queryKey: ['sitemap', params.packageName, params.version, parsedEntrypoints.join('.')],
queryFn: async () => { queryFn: async () => {
const response = await fetch( const response = await fetch(
@@ -38,6 +42,10 @@ export function Navigation() {
return acc; return acc;
}, {}); }, {});
if (isLoading) {
return <Loader2Icon className="mx-auto h-10 w-10 animate-spin" />;
}
return ( return (
<nav className="flex flex-col gap-2 pr-3"> <nav className="flex flex-col gap-2 pr-3">
{groupedNodes?.class?.length ? ( {groupedNodes?.class?.length ? (

View File

@@ -21,7 +21,7 @@ export function SidebarHeader() {
const hasEntryPoints = PACKAGES_WITH_ENTRY_POINTS.includes(params.packageName); const hasEntryPoints = PACKAGES_WITH_ENTRY_POINTS.includes(params.packageName);
const { data: entryPoints } = useQuery({ const { data: entryPoints, isLoading: isLoadingEntryPoints } = useQuery({
queryKey: ['entryPoints', params.packageName, params.version], queryKey: ['entryPoints', params.packageName, params.version],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`/api/docs/entrypoints?packageName=${params.packageName}&version=${params.version}`); const response = await fetch(`/api/docs/entrypoints?packageName=${params.packageName}&version=${params.version}`);
@@ -30,7 +30,7 @@ export function SidebarHeader() {
}, },
}); });
const { data: versions } = useQuery({ const { data: versions, isLoading: isLoadingVersions } = useQuery({
queryKey: ['versions', params.packageName], queryKey: ['versions', params.packageName],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`/api/docs/versions?packageName=${params.packageName}`); const response = await fetch(`/api/docs/versions?packageName=${params.packageName}`);
@@ -61,8 +61,8 @@ export function SidebarHeader() {
</div> </div>
<PackageSelect /> <PackageSelect />
{/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */} {/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */}
<VersionSelect versions={versions ?? []} /> <VersionSelect isLoading={isLoadingVersions} versions={versions ?? []} />
{hasEntryPoints ? <EntryPointSelect entryPoints={entryPoints ?? []} /> : null} {hasEntryPoints ? <EntryPointSelect entryPoints={entryPoints ?? []} isLoading={isLoadingEntryPoints} /> : null}
<SearchButton /> <SearchButton />
</div> </div>
</BasSidebarHeader> </BasSidebarHeader>

View File

@@ -1,10 +1,17 @@
'use client'; 'use client';
import { Loader2Icon } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { Select, SelectList, SelectOption, SelectTrigger } from '@/components/ui/Select'; import { Select, SelectList, SelectOption, SelectTrigger } from '@/components/ui/Select';
import { DEFAULT_ENTRY_POINT, PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants'; import { DEFAULT_ENTRY_POINT, PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants';
export function VersionSelect({ versions }: { readonly versions: { readonly version: string }[] }) { export function VersionSelect({
versions,
isLoading,
}: {
readonly isLoading: boolean;
readonly versions: { readonly version: string }[];
}) {
const router = useRouter(); const router = useRouter();
const params = useParams<{ packageName: string; version: string }>(); const params = useParams<{ packageName: string; version: string }>();
@@ -12,11 +19,24 @@ export function VersionSelect({ versions }: { readonly versions: { readonly vers
return ( return (
<Select <Select
aria-label="Select a version" aria-label={isLoading ? 'Loading versions...' : 'Select a version'}
defaultSelectedKey={params.version} defaultSelectedKey={params.version}
key={`${params.packageName}-${params.version}`} key={`${params.packageName}-${params.version}`}
placeholder={isLoading ? 'Loading versions...' : 'Select a version'}
> >
<SelectTrigger className="bg-[#f3f3f4] dark:bg-[#121214]" /> <SelectTrigger
className="bg-[#f3f3f4] dark:bg-[#121214]"
suffix={
isLoading ? (
<Loader2Icon
aria-hidden
className="size-6 shrink-0 animate-spin duration-200 forced-colors:text-[ButtonText] forced-colors:group-disabled:text-[GrayText]"
size={24}
strokeWidth={1.5}
/>
) : null
}
/>
<SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={versions}> <SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={versions}>
{(item) => ( {(item) => (
<SelectOption <SelectOption

View File

@@ -96,6 +96,7 @@ export function SelectList<Type extends object>(props: SelectListProps<Type>) {
export type SelectTriggerProps = ComponentProps<typeof Button> & { export type SelectTriggerProps = ComponentProps<typeof Button> & {
readonly className?: string; readonly className?: string;
readonly prefix?: ReactNode; readonly prefix?: ReactNode;
readonly suffix?: ReactNode;
}; };
export function SelectTrigger(props: SelectTriggerProps) { export function SelectTrigger(props: SelectTriggerProps) {
@@ -113,6 +114,7 @@ export function SelectTrigger(props: SelectTriggerProps) {
className="text-base-neutral-900 group-disabled:data-placeholder:text-base-neutral-900 dark:group-disabled:data-placeholder:text-base-neutral-40 dark:data-placeholder:text-base-neutral-500 dark:text-base-neutral-40 data-placeholder:text-base-neutral-400 text-base-lg sm:text-base-md grid flex-1 grid-cols-[auto_1fr] place-items-start items-center px-3 py-2.5 *:data-[slot=avatar]:*:-mx-0.5 *:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:*:mr-2 *:data-[slot=avatar]:mr-2 *:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:mr-1 *:data-[slot=icon]:size-5.5 [&_[slot=description]]:hidden *:[span]:col-start-2" className="text-base-neutral-900 group-disabled:data-placeholder:text-base-neutral-900 dark:group-disabled:data-placeholder:text-base-neutral-40 dark:data-placeholder:text-base-neutral-500 dark:text-base-neutral-40 data-placeholder:text-base-neutral-400 text-base-lg sm:text-base-md grid flex-1 grid-cols-[auto_1fr] place-items-start items-center px-3 py-2.5 *:data-[slot=avatar]:*:-mx-0.5 *:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:*:mr-2 *:data-[slot=avatar]:mr-2 *:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:mr-1 *:data-[slot=icon]:size-5.5 [&_[slot=description]]:hidden *:[span]:col-start-2"
data-slot="select-value" data-slot="select-value"
/> />
{props.suffix && <span className="mr-10 ml-2 *:data-[slot=icon]:size-5.5">{props.suffix}</span>}
<ChevronDownIcon <ChevronDownIcon
aria-hidden aria-hidden
className="size-6 shrink-0 duration-200 group-open:rotate-180 forced-colors:text-[ButtonText] forced-colors:group-disabled:text-[GrayText]" className="size-6 shrink-0 duration-200 group-open:rotate-180 forced-colors:text-[ButtonText] forced-colors:group-disabled:text-[GrayText]"