Add loader status to photo links
This commit is contained in:
parent
d7faf2ab92
commit
0f1753fad0
@ -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>
|
||||
|
||||
@ -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" />}>
|
||||
|
||||
@ -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',
|
||||
@ -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(
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -256,20 +256,18 @@ 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">
|
||||
<AdminPhotoMenuClient {...{
|
||||
photo,
|
||||
revalidatePhoto,
|
||||
includeFavorite: includeFavoriteInAdminMenu,
|
||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||
}} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="float-right translate-y-[-4px]">
|
||||
<AdminPhotoMenuClient {...{
|
||||
photo,
|
||||
revalidatePhoto,
|
||||
includeFavorite: includeFavoriteInAdminMenu,
|
||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||
}} />
|
||||
</div>
|
||||
{hasTitle && (showTitleAsH1
|
||||
? <h1>{renderPhotoLink}</h1>
|
||||
: renderPhotoLink)}
|
||||
<div className="space-y-baseline">
|
||||
{photo.caption &&
|
||||
<div className={clsx(
|
||||
|
||||
@ -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}
|
||||
>
|
||||
{children ?? titleForPhoto(photo)}
|
||||
</Link>
|
||||
},
|
||||
scroll,
|
||||
prefetch,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const children = photo
|
||||
? (_children ?? titleForPhoto(photo))
|
||||
: _children;
|
||||
|
||||
return (
|
||||
photo && linkProps
|
||||
? loaderType === 'spinner'
|
||||
? <LinkWithStatus {...linkProps}>
|
||||
{({ isLoading }) => <>
|
||||
{children}
|
||||
{isLoading && <>
|
||||
<Spinner className="translate-y-[0.5px]" />
|
||||
</>}
|
||||
</>}
|
||||
</LinkWithStatus>
|
||||
: <LinkWithLoaderBadge
|
||||
{...linkProps}
|
||||
offsetPadding
|
||||
>
|
||||
{children}
|
||||
</LinkWithLoaderBadge>
|
||||
: <span className={clsx(
|
||||
'text-gray-300 dark:text-gray-700 cursor-default',
|
||||
className,
|
||||
)}>
|
||||
{children ?? (photo ? titleForPhoto(photo) : undefined)}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user