Split link status/loader into two components

This commit is contained in:
Sam Becker 2025-01-21 23:24:36 -06:00
parent b65136faa3
commit ad11ce32b0
4 changed files with 53 additions and 38 deletions

View File

@ -1,5 +1,6 @@
'use client';
import LinkWithLoader from '@/components/LinkWithLoader';
import LinkWithStatus from '@/components/LinkWithStatus';
import Note from '@/components/Note';
import SiteGrid from '@/components/SiteGrid';
@ -73,11 +74,11 @@ export default function AdminNavClient({
key={label}
href={href}
className={clsx(
'flex gap-0.5',
checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim',
'px-1 py-0.5 rounded-md',
)}
loadingClassName="bg-dim"
contentClassName="flex gap-0.5"
prefetch={false}
>
<span>{label}</span>
@ -85,19 +86,19 @@ export default function AdminNavClient({
<span>({count})</span>}
</LinkWithStatus>)}
</div>
<LinkWithStatus
<LinkWithLoader
href={PATH_ADMIN_CONFIGURATION}
className={isPathAdminConfiguration(pathname)
? 'font-bold'
: 'text-dim'}
loadingElement={<Spinner />}
loader={<Spinner />}
>
<BiCog
size={18}
className="inline-flex translate-y-0.5"
aria-label="App Configuration"
/>
</LinkWithStatus>
</LinkWithLoader>
</div>
{shouldShowBanner &&
<Note icon={<FaRegClock className="flex-shrink-0" />}>

View File

@ -0,0 +1,31 @@
import { ComponentProps, ReactNode } from 'react';
import LinkWithStatus from './LinkWithStatus';
import clsx from 'clsx/lite';
import Link from 'next/link';
export default function LinkWithLoader({
loader,
children,
...props
}: ComponentProps<typeof Link> & {
loader: ReactNode
}) {
return (
<LinkWithStatus {...props}>
{({ isLoading }) => <>
<span className={clsx(
'flex transition-opacity',
isLoading ? 'opacity-0' : 'opacity-100',
)}>
{children}
</span>
{isLoading && <span className={clsx(
'absolute inset-0',
'flex items-center justify-center',
)}>
{loader}
</span>}
</>}
</LinkWithStatus>
);
}

View File

@ -20,18 +20,14 @@ const MAX_LOADING_DURATION = 15_000;
export type LinkWithStatusProps = Omit<
ComponentProps<typeof Link>, 'children'
> & {
loadingElement?: ReactNode
loadingClassName?: string
contentClassName?: string
children: ReactNode | ((props: {
isLoading: boolean
}) => ReactNode)
}
export default function LinkWithStatus({
loadingElement,
loadingClassName,
contentClassName,
href,
className,
onClick,
@ -93,7 +89,10 @@ export default function LinkWithStatus({
{...props }
href={href}
className={clsx(
'relative transition-colors',
'relative flex transition-[colors,opacity]',
(loadingClassName || isControlled)
? 'opacity-100'
: isLoading ? 'opacity-50' : 'opacity-100',
className,
isLoading && loadingClassName,
)}
@ -116,24 +115,8 @@ export default function LinkWithStatus({
onClick?.(e);
}}
>
<span className={clsx(
'flex transition-opacity',
contentClassName,
loadingElement
? isLoading ? 'opacity-0' : 'opacity-100'
: (loadingClassName || isControlled)
? 'opacity-100'
: isLoading ? 'opacity-50' : 'opacity-100',
)}>
{typeof children === 'function'
? children({ isLoading })
: children}
</span>
{isLoading && loadingElement && <span className={clsx(
'absolute inset-0',
'flex items-center justify-center',
)}>
{loadingElement}
</span>}
</Link>;
}

View File

@ -1,8 +1,8 @@
import { clsx } from 'clsx/lite';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config';
import { JSX, ReactNode } from 'react';
import LinkWithStatus from './LinkWithStatus';
import { JSX } from 'react';
import Spinner from './Spinner';
import LinkWithLoader from './LinkWithLoader';
export default function SwitcherItem({
icon,
@ -37,23 +37,23 @@ export default function SwitcherItem({
: 'hover:text-gray-700 dark:hover:text-gray-400',
);
const renderContent = (content: ReactNode) => noPadding
? content
const renderIcon = () => noPadding
? icon
: <div className="w-[28px] h-[24px] flex items-center justify-center">
{content}
{icon}
</div>;
return (
href
? <LinkWithStatus {...{
? <LinkWithLoader {...{
title,
href,
className,
prefetch,
loadingElement: <Spinner />,
loader: <Spinner />,
}}>
{renderContent(icon)}
</LinkWithStatus>
: <div {...{ title, onClick, className }}>{renderContent(icon)}</div>
{renderIcon()}
</LinkWithLoader>
: <div {...{ title, onClick, className }}>{renderIcon()}</div>
);
};