Add loader status to photo links

This commit is contained in:
Sam Becker 2025-03-24 23:45:15 -05:00
parent d7faf2ab92
commit 0f1753fad0
10 changed files with 93 additions and 67 deletions

View File

@ -1,13 +1,13 @@
'use client';
import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths';
import LinkWithStatus from '@/components/LinkWithStatus';
import ResponsiveText from '@/components/primitives/ResponsiveText';
import clsx from 'clsx/lite';
import ClearCacheButton from '@/admin/ClearCacheButton';
import { usePathname } from 'next/navigation';
import { useAppState } from '@/state/AppState';
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
import LinkWithLoaderBadge from '@/components/LinkWithLoaderBadge';
const ADMIN_INFO_PAGES = [{
title: 'App Insights',
@ -47,7 +47,7 @@ export default function AdminInfoPage({
)}>
{pages
.map(({ title, titleShort, path }) =>
<LinkWithStatus
<LinkWithLoaderBadge
key={path}
href={path}
className={clsx(
@ -57,10 +57,8 @@ export default function AdminInfoPage({
? 'font-medium'
: 'text-dim'
: undefined,
'px-1 py-0.5 rounded-md',
'hover:text-main',
)}
loadingClassName="bg-gray-200/50 dark:bg-gray-700/50"
>
<ResponsiveText shortText={titleShort}>
{title}
@ -71,7 +69,7 @@ export default function AdminInfoPage({
top={4}
right={-2}
/>}
</LinkWithStatus>)}
</LinkWithLoaderBadge>)}
</div>
<ClearCacheButton />
</div>

View File

@ -1,6 +1,6 @@
'use client';
import LinkWithLoader from '@/components/LinkWithLoader';
import LinkWithIconLoader from '@/components/LinkWithIconLoader';
import Note from '@/components/Note';
import AppGrid from '@/components/AppGrid';
import Spinner from '@/components/Spinner';
@ -21,7 +21,7 @@ import AdminAppInfoIcon from './AdminAppInfoIcon';
import AdminInfoNav from './AdminInfoNav';
import LinkWithLoaderBadge from '@/components/LinkWithLoaderBadge';
// Updates considered recent if they occurred in past 5 minutes
// Updates from past 5 minutes considered recent
const areTimesRecent = (dates: Date[]) => dates
.some(date => differenceInMinutes(new Date(), date) < 5);
@ -89,17 +89,16 @@ export default function AdminNavClient({
<span>({count})</span>}
</LinkWithLoaderBadge>)}
</div>
<LinkWithLoader
<LinkWithIconLoader
href={includeInsights
? PATH_ADMIN_INSIGHTS
: PATH_ADMIN_CONFIGURATION}
className={isPathAdminInfo(pathname)
? 'font-bold'
: 'text-dim'}
icon={<AdminAppInfoIcon />}
loader={<Spinner className="translate-y-[-0.75px]" />}
>
<AdminAppInfoIcon />
</LinkWithLoader>
/>
</div>
{shouldShowBanner &&
<Note icon={<FaRegClock className="shrink-0" />}>

View File

@ -1,25 +1,28 @@
import { ComponentProps, ReactNode } from 'react';
import LinkWithStatus from './LinkWithStatus';
import clsx from 'clsx/lite';
import Link from 'next/link';
export default function LinkWithLoader({
export default function LinkWithIconLoader({
className,
icon,
loader,
children,
debugLoading,
...props
}: ComponentProps<typeof Link> & {
}: Omit<ComponentProps<typeof LinkWithStatus>, 'children'> & {
icon: ReactNode
loader: ReactNode
debugLoading?: boolean
}) {
return (
<LinkWithStatus {...props}>
<LinkWithStatus
{...props}
className={clsx('relative', className)}
>
{({ isLoading }) => <>
<span className={clsx(
'flex transition-opacity',
isLoading ? 'opacity-0' : 'opacity-100',
isLoading || debugLoading ? 'opacity-0' : 'opacity-100',
)}>
{children}
{icon}
</span>
{(isLoading || debugLoading) && <span className={clsx(
'absolute inset-0',

View File

@ -5,13 +5,18 @@ import LinkWithStatus from './LinkWithStatus';
export default function LinkWithLoaderBadge({
className,
loadingClassName,
offsetPadding,
...props
}: ComponentProps<typeof LinkWithStatus>) {
}: ComponentProps<typeof LinkWithStatus> & {
offsetPadding?: boolean
}) {
return (
<LinkWithStatus
{...props}
className={clsx(
'px-1 py-0.5 rounded-md',
offsetPadding && '-mx-1 -my-0.5',
'px-1 py-0.5',
'rounded-md',
className,
)}
loadingClassName={clsx(

View File

@ -89,10 +89,10 @@ export default function LinkWithStatus({
useEffect(() => () => clearTimeouts(), [clearTimeouts]);
return <Link
{...props }
{...props}
href={href}
className={clsx(
'relative inline-flex transition-[colors,opacity]',
'transition-[colors,opacity]',
(loadingClassName || isControlled)
? 'opacity-100'
: isLoading ? 'opacity-50' : 'opacity-100',

View File

@ -2,7 +2,7 @@ import { clsx } from 'clsx/lite';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
import { ReactNode } from 'react';
import Spinner from './Spinner';
import LinkWithLoader from './LinkWithLoader';
import LinkWithIconLoader from './LinkWithIconLoader';
export default function SwitcherItem({
icon,
@ -52,15 +52,14 @@ export default function SwitcherItem({
return (
href
? <LinkWithLoader {...{
? <LinkWithIconLoader {...{
title,
href,
className,
prefetch,
icon: renderIcon(),
loader: <Spinner />,
}}>
{renderIcon()}
</LinkWithLoader>
}} />
: <div {...{ title, onClick, className }}>
{renderIcon()}
</div>

View File

@ -68,7 +68,7 @@ export default function EntityLink({
return (
<span className={clsx(
'group inline-flex max-w-full overflow-hidden',
'group inline-flex max-w-full overflow-hidden select-none',
className,
)}>
<LinkWithStatus

View File

@ -256,12 +256,8 @@ export default function PhotoLarge({
'pb-6',
)}>
{/* Meta */}
<div className="pr-2 md:pr-0">
<div className="md:relative flex gap-2 items-start">
{hasTitle && (showTitleAsH1
? <h1>{renderPhotoLink}</h1>
: renderPhotoLink)}
<div className="absolute right-0 translate-y-[-4px] z-10">
<div>
<div className="float-right translate-y-[-4px]">
<AdminPhotoMenuClient {...{
photo,
revalidatePhoto,
@ -269,7 +265,9 @@ export default function PhotoLarge({
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
}} />
</div>
</div>
{hasTitle && (showTitleAsH1
? <h1>{renderPhotoLink}</h1>
: renderPhotoLink)}
<div className="space-y-baseline">
{photo.caption &&
<div className={clsx(

View File

@ -1,13 +1,15 @@
'use client';
import { ReactNode } from 'react';
import { ReactNode, ComponentProps } from 'react';
import { Photo, titleForPhoto } from '@/photo';
import { PhotoSetCategory } from '@/category';
import Link from 'next/link';
import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/state/AppState';
import { pathForPhoto } from '@/app/paths';
import { clsx } from 'clsx/lite';
import LinkWithStatus from '@/components/LinkWithStatus';
import Spinner from '@/components/Spinner';
import LinkWithLoaderBadge from '@/components/LinkWithLoaderBadge';
export default function PhotoLink({
photo,
@ -15,7 +17,8 @@ export default function PhotoLink({
prefetch,
nextPhotoAnimation,
className,
children,
children: _children,
loaderType = 'spinner',
...categories
}: {
photo?: Photo
@ -24,29 +27,52 @@ export default function PhotoLink({
nextPhotoAnimation?: AnimationConfig
className?: string
children?: ReactNode
loaderType?: 'spinner' | 'badge'
} & PhotoSetCategory) {
const { setNextPhotoAnimation } = useAppState();
return (
photo
? <Link
href={pathForPhoto({ photo, ...categories })}
prefetch={prefetch}
onClick={() => {
const linkProps:
Omit<ComponentProps<typeof LinkWithStatus>, 'children'> |
undefined = photo
? {
className,
href: pathForPhoto({ photo, ...categories }),
onClick: () => {
if (nextPhotoAnimation) {
setNextPhotoAnimation?.(nextPhotoAnimation);
}
}}
className={className}
scroll={scroll}
},
scroll,
prefetch,
}
: undefined;
const children = photo
? (_children ?? titleForPhoto(photo))
: _children;
return (
photo && linkProps
? loaderType === 'spinner'
? <LinkWithStatus {...linkProps}>
{({ isLoading }) => <>
{children}
{isLoading && <>
&nbsp;<Spinner className="translate-y-[0.5px]" />
</>}
</>}
</LinkWithStatus>
: <LinkWithLoaderBadge
{...linkProps}
offsetPadding
>
{children ?? titleForPhoto(photo)}
</Link>
{children}
</LinkWithLoaderBadge>
: <span className={clsx(
'text-gray-300 dark:text-gray-700 cursor-default',
className,
)}>
{children ?? (photo ? titleForPhoto(photo) : undefined)}
{children}
</span>
);
};

View File

@ -85,21 +85,21 @@ export default function PhotoPrevNext({
className,
)}>
<div className={clsx(
'h-4',
'flex gap-2 select-none',
// Fixes alignment issue when switching from chevrons to text
'items-center sm:items-start',
'*:select-none',
)}>
<PhotoLink
{...categories}
photo={previousPhoto}
className="select-none h-[1rem]"
nextPhotoAnimation={ANIMATION_RIGHT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronLeft
className="sm:hidden text-[1.1rem] translate-y-[-1px]"
/>
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">PREV</span>
</PhotoLink>
<span className="text-extra-extra-dim">
@ -108,14 +108,12 @@ export default function PhotoPrevNext({
<PhotoLink
{...categories}
photo={nextPhoto}
className="select-none h-[1rem]"
nextPhotoAnimation={ANIMATION_LEFT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronRight
className="sm:hidden text-[1.1rem] translate-y-[-1px]"
/>
<FiChevronRight className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">NEXT</span>
</PhotoLink>
</div>