Add loader status to photo links
This commit is contained in:
parent
d7faf2ab92
commit
0f1753fad0
@ -1,13 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths';
|
import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths';
|
||||||
import LinkWithStatus from '@/components/LinkWithStatus';
|
|
||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import ClearCacheButton from '@/admin/ClearCacheButton';
|
import ClearCacheButton from '@/admin/ClearCacheButton';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||||
|
import LinkWithLoaderBadge from '@/components/LinkWithLoaderBadge';
|
||||||
|
|
||||||
const ADMIN_INFO_PAGES = [{
|
const ADMIN_INFO_PAGES = [{
|
||||||
title: 'App Insights',
|
title: 'App Insights',
|
||||||
@ -47,7 +47,7 @@ export default function AdminInfoPage({
|
|||||||
)}>
|
)}>
|
||||||
{pages
|
{pages
|
||||||
.map(({ title, titleShort, path }) =>
|
.map(({ title, titleShort, path }) =>
|
||||||
<LinkWithStatus
|
<LinkWithLoaderBadge
|
||||||
key={path}
|
key={path}
|
||||||
href={path}
|
href={path}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -57,10 +57,8 @@ export default function AdminInfoPage({
|
|||||||
? 'font-medium'
|
? 'font-medium'
|
||||||
: 'text-dim'
|
: 'text-dim'
|
||||||
: undefined,
|
: undefined,
|
||||||
'px-1 py-0.5 rounded-md',
|
|
||||||
'hover:text-main',
|
'hover:text-main',
|
||||||
)}
|
)}
|
||||||
loadingClassName="bg-gray-200/50 dark:bg-gray-700/50"
|
|
||||||
>
|
>
|
||||||
<ResponsiveText shortText={titleShort}>
|
<ResponsiveText shortText={titleShort}>
|
||||||
{title}
|
{title}
|
||||||
@ -71,7 +69,7 @@ export default function AdminInfoPage({
|
|||||||
top={4}
|
top={4}
|
||||||
right={-2}
|
right={-2}
|
||||||
/>}
|
/>}
|
||||||
</LinkWithStatus>)}
|
</LinkWithLoaderBadge>)}
|
||||||
</div>
|
</div>
|
||||||
<ClearCacheButton />
|
<ClearCacheButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import LinkWithLoader from '@/components/LinkWithLoader';
|
import LinkWithIconLoader from '@/components/LinkWithIconLoader';
|
||||||
import Note from '@/components/Note';
|
import Note from '@/components/Note';
|
||||||
import AppGrid from '@/components/AppGrid';
|
import AppGrid from '@/components/AppGrid';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
@ -21,7 +21,7 @@ import AdminAppInfoIcon from './AdminAppInfoIcon';
|
|||||||
import AdminInfoNav from './AdminInfoNav';
|
import AdminInfoNav from './AdminInfoNav';
|
||||||
import LinkWithLoaderBadge from '@/components/LinkWithLoaderBadge';
|
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
|
const areTimesRecent = (dates: Date[]) => dates
|
||||||
.some(date => differenceInMinutes(new Date(), date) < 5);
|
.some(date => differenceInMinutes(new Date(), date) < 5);
|
||||||
|
|
||||||
@ -89,17 +89,16 @@ export default function AdminNavClient({
|
|||||||
<span>({count})</span>}
|
<span>({count})</span>}
|
||||||
</LinkWithLoaderBadge>)}
|
</LinkWithLoaderBadge>)}
|
||||||
</div>
|
</div>
|
||||||
<LinkWithLoader
|
<LinkWithIconLoader
|
||||||
href={includeInsights
|
href={includeInsights
|
||||||
? PATH_ADMIN_INSIGHTS
|
? PATH_ADMIN_INSIGHTS
|
||||||
: PATH_ADMIN_CONFIGURATION}
|
: PATH_ADMIN_CONFIGURATION}
|
||||||
className={isPathAdminInfo(pathname)
|
className={isPathAdminInfo(pathname)
|
||||||
? 'font-bold'
|
? 'font-bold'
|
||||||
: 'text-dim'}
|
: 'text-dim'}
|
||||||
|
icon={<AdminAppInfoIcon />}
|
||||||
loader={<Spinner className="translate-y-[-0.75px]" />}
|
loader={<Spinner className="translate-y-[-0.75px]" />}
|
||||||
>
|
/>
|
||||||
<AdminAppInfoIcon />
|
|
||||||
</LinkWithLoader>
|
|
||||||
</div>
|
</div>
|
||||||
{shouldShowBanner &&
|
{shouldShowBanner &&
|
||||||
<Note icon={<FaRegClock className="shrink-0" />}>
|
<Note icon={<FaRegClock className="shrink-0" />}>
|
||||||
|
|||||||
@ -1,25 +1,28 @@
|
|||||||
import { ComponentProps, ReactNode } from 'react';
|
import { ComponentProps, ReactNode } from 'react';
|
||||||
import LinkWithStatus from './LinkWithStatus';
|
import LinkWithStatus from './LinkWithStatus';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export default function LinkWithLoader({
|
export default function LinkWithIconLoader({
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
loader,
|
loader,
|
||||||
children,
|
|
||||||
debugLoading,
|
debugLoading,
|
||||||
...props
|
...props
|
||||||
}: ComponentProps<typeof Link> & {
|
}: Omit<ComponentProps<typeof LinkWithStatus>, 'children'> & {
|
||||||
|
icon: ReactNode
|
||||||
loader: ReactNode
|
loader: ReactNode
|
||||||
debugLoading?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<LinkWithStatus {...props}>
|
<LinkWithStatus
|
||||||
|
{...props}
|
||||||
|
className={clsx('relative', className)}
|
||||||
|
>
|
||||||
{({ isLoading }) => <>
|
{({ isLoading }) => <>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'flex transition-opacity',
|
'flex transition-opacity',
|
||||||
isLoading ? 'opacity-0' : 'opacity-100',
|
isLoading || debugLoading ? 'opacity-0' : 'opacity-100',
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
{(isLoading || debugLoading) && <span className={clsx(
|
{(isLoading || debugLoading) && <span className={clsx(
|
||||||
'absolute inset-0',
|
'absolute inset-0',
|
||||||
@ -5,13 +5,18 @@ import LinkWithStatus from './LinkWithStatus';
|
|||||||
export default function LinkWithLoaderBadge({
|
export default function LinkWithLoaderBadge({
|
||||||
className,
|
className,
|
||||||
loadingClassName,
|
loadingClassName,
|
||||||
|
offsetPadding,
|
||||||
...props
|
...props
|
||||||
}: ComponentProps<typeof LinkWithStatus>) {
|
}: ComponentProps<typeof LinkWithStatus> & {
|
||||||
|
offsetPadding?: boolean
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<LinkWithStatus
|
<LinkWithStatus
|
||||||
{...props}
|
{...props}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-1 py-0.5 rounded-md',
|
offsetPadding && '-mx-1 -my-0.5',
|
||||||
|
'px-1 py-0.5',
|
||||||
|
'rounded-md',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
loadingClassName={clsx(
|
loadingClassName={clsx(
|
||||||
|
|||||||
@ -89,10 +89,10 @@ export default function LinkWithStatus({
|
|||||||
useEffect(() => () => clearTimeouts(), [clearTimeouts]);
|
useEffect(() => () => clearTimeouts(), [clearTimeouts]);
|
||||||
|
|
||||||
return <Link
|
return <Link
|
||||||
{...props }
|
{...props}
|
||||||
href={href}
|
href={href}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative inline-flex transition-[colors,opacity]',
|
'transition-[colors,opacity]',
|
||||||
(loadingClassName || isControlled)
|
(loadingClassName || isControlled)
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: isLoading ? 'opacity-50' : '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 { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import LinkWithLoader from './LinkWithLoader';
|
import LinkWithIconLoader from './LinkWithIconLoader';
|
||||||
|
|
||||||
export default function SwitcherItem({
|
export default function SwitcherItem({
|
||||||
icon,
|
icon,
|
||||||
@ -52,15 +52,14 @@ export default function SwitcherItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
href
|
href
|
||||||
? <LinkWithLoader {...{
|
? <LinkWithIconLoader {...{
|
||||||
title,
|
title,
|
||||||
href,
|
href,
|
||||||
className,
|
className,
|
||||||
prefetch,
|
prefetch,
|
||||||
|
icon: renderIcon(),
|
||||||
loader: <Spinner />,
|
loader: <Spinner />,
|
||||||
}}>
|
}} />
|
||||||
{renderIcon()}
|
|
||||||
</LinkWithLoader>
|
|
||||||
: <div {...{ title, onClick, className }}>
|
: <div {...{ title, onClick, className }}>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export default function EntityLink({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'group inline-flex max-w-full overflow-hidden',
|
'group inline-flex max-w-full overflow-hidden select-none',
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
<LinkWithStatus
|
<LinkWithStatus
|
||||||
|
|||||||
@ -256,20 +256,18 @@ export default function PhotoLarge({
|
|||||||
'pb-6',
|
'pb-6',
|
||||||
)}>
|
)}>
|
||||||
{/* Meta */}
|
{/* Meta */}
|
||||||
<div className="pr-2 md:pr-0">
|
<div>
|
||||||
<div className="md:relative flex gap-2 items-start">
|
<div className="float-right translate-y-[-4px]">
|
||||||
{hasTitle && (showTitleAsH1
|
<AdminPhotoMenuClient {...{
|
||||||
? <h1>{renderPhotoLink}</h1>
|
photo,
|
||||||
: renderPhotoLink)}
|
revalidatePhoto,
|
||||||
<div className="absolute right-0 translate-y-[-4px] z-10">
|
includeFavorite: includeFavoriteInAdminMenu,
|
||||||
<AdminPhotoMenuClient {...{
|
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||||
photo,
|
}} />
|
||||||
revalidatePhoto,
|
|
||||||
includeFavorite: includeFavoriteInAdminMenu,
|
|
||||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{hasTitle && (showTitleAsH1
|
||||||
|
? <h1>{renderPhotoLink}</h1>
|
||||||
|
: renderPhotoLink)}
|
||||||
<div className="space-y-baseline">
|
<div className="space-y-baseline">
|
||||||
{photo.caption &&
|
{photo.caption &&
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, ComponentProps } from 'react';
|
||||||
import { Photo, titleForPhoto } from '@/photo';
|
import { Photo, titleForPhoto } from '@/photo';
|
||||||
import { PhotoSetCategory } from '@/category';
|
import { PhotoSetCategory } from '@/category';
|
||||||
import Link from 'next/link';
|
|
||||||
import { AnimationConfig } from '../components/AnimateItems';
|
import { AnimationConfig } from '../components/AnimateItems';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { pathForPhoto } from '@/app/paths';
|
import { pathForPhoto } from '@/app/paths';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
|
import LinkWithStatus from '@/components/LinkWithStatus';
|
||||||
|
import Spinner from '@/components/Spinner';
|
||||||
|
import LinkWithLoaderBadge from '@/components/LinkWithLoaderBadge';
|
||||||
|
|
||||||
export default function PhotoLink({
|
export default function PhotoLink({
|
||||||
photo,
|
photo,
|
||||||
@ -15,7 +17,8 @@ export default function PhotoLink({
|
|||||||
prefetch,
|
prefetch,
|
||||||
nextPhotoAnimation,
|
nextPhotoAnimation,
|
||||||
className,
|
className,
|
||||||
children,
|
children: _children,
|
||||||
|
loaderType = 'spinner',
|
||||||
...categories
|
...categories
|
||||||
}: {
|
}: {
|
||||||
photo?: Photo
|
photo?: Photo
|
||||||
@ -24,29 +27,52 @@ export default function PhotoLink({
|
|||||||
nextPhotoAnimation?: AnimationConfig
|
nextPhotoAnimation?: AnimationConfig
|
||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
loaderType?: 'spinner' | 'badge'
|
||||||
} & PhotoSetCategory) {
|
} & PhotoSetCategory) {
|
||||||
const { setNextPhotoAnimation } = useAppState();
|
const { setNextPhotoAnimation } = useAppState();
|
||||||
|
|
||||||
return (
|
const linkProps:
|
||||||
photo
|
Omit<ComponentProps<typeof LinkWithStatus>, 'children'> |
|
||||||
? <Link
|
undefined = photo
|
||||||
href={pathForPhoto({ photo, ...categories })}
|
? {
|
||||||
prefetch={prefetch}
|
className,
|
||||||
onClick={() => {
|
href: pathForPhoto({ photo, ...categories }),
|
||||||
|
onClick: () => {
|
||||||
if (nextPhotoAnimation) {
|
if (nextPhotoAnimation) {
|
||||||
setNextPhotoAnimation?.(nextPhotoAnimation);
|
setNextPhotoAnimation?.(nextPhotoAnimation);
|
||||||
}
|
}
|
||||||
}}
|
},
|
||||||
className={className}
|
scroll,
|
||||||
scroll={scroll}
|
prefetch,
|
||||||
>
|
}
|
||||||
{children ?? titleForPhoto(photo)}
|
: undefined;
|
||||||
</Link>
|
|
||||||
|
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(
|
: <span className={clsx(
|
||||||
'text-gray-300 dark:text-gray-700 cursor-default',
|
'text-gray-300 dark:text-gray-700 cursor-default',
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
{children ?? (photo ? titleForPhoto(photo) : undefined)}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -85,21 +85,21 @@ export default function PhotoPrevNext({
|
|||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
|
'h-4',
|
||||||
'flex gap-2 select-none',
|
'flex gap-2 select-none',
|
||||||
// Fixes alignment issue when switching from chevrons to text
|
// Fixes alignment issue when switching from chevrons to text
|
||||||
'items-center sm:items-start',
|
'items-center sm:items-start',
|
||||||
|
'*:select-none',
|
||||||
)}>
|
)}>
|
||||||
<PhotoLink
|
<PhotoLink
|
||||||
{...categories}
|
{...categories}
|
||||||
photo={previousPhoto}
|
photo={previousPhoto}
|
||||||
className="select-none h-[1rem]"
|
|
||||||
nextPhotoAnimation={ANIMATION_RIGHT}
|
nextPhotoAnimation={ANIMATION_RIGHT}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
|
loaderType="badge"
|
||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
<FiChevronLeft
|
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
||||||
className="sm:hidden text-[1.1rem] translate-y-[-1px]"
|
|
||||||
/>
|
|
||||||
<span className="hidden sm:inline-block">PREV</span>
|
<span className="hidden sm:inline-block">PREV</span>
|
||||||
</PhotoLink>
|
</PhotoLink>
|
||||||
<span className="text-extra-extra-dim">
|
<span className="text-extra-extra-dim">
|
||||||
@ -108,14 +108,12 @@ export default function PhotoPrevNext({
|
|||||||
<PhotoLink
|
<PhotoLink
|
||||||
{...categories}
|
{...categories}
|
||||||
photo={nextPhoto}
|
photo={nextPhoto}
|
||||||
className="select-none h-[1rem]"
|
|
||||||
nextPhotoAnimation={ANIMATION_LEFT}
|
nextPhotoAnimation={ANIMATION_LEFT}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
|
loaderType="badge"
|
||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
<FiChevronRight
|
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
||||||
className="sm:hidden text-[1.1rem] translate-y-[-1px]"
|
|
||||||
/>
|
|
||||||
<span className="hidden sm:inline-block">NEXT</span>
|
<span className="hidden sm:inline-block">NEXT</span>
|
||||||
</PhotoLink>
|
</PhotoLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user