Show loading indicator when adding uploads

This commit is contained in:
Sam Becker 2024-05-06 12:00:46 -05:00
parent b431e5de5c
commit afd0e23a67
9 changed files with 150 additions and 42 deletions

View File

@ -1,23 +1,17 @@
import Link from 'next/link';
import { BiImageAdd } from 'react-icons/bi'; import { BiImageAdd } from 'react-icons/bi';
import PathLoaderButton from '@/components/PathLoaderButton';
export default function AddButton ({ export default function AddButton({
href, path,
label = 'Add',
}: { }: {
href: string, path: string,
label?: string,
}) { }) {
return ( return (
<Link <PathLoaderButton
title={label} path={path}
href={href} icon={<BiImageAdd size={18} className="translate-x-[1px]" />}
className="button"
> >
<BiImageAdd size={18} className="translate-y-[1px]" /> Add
<span className="hidden sm:inline-block"> </PathLoaderButton>
{label}
</span>
</Link>
); );
} }

View File

@ -91,7 +91,9 @@ export default function AdminPhotosTable({
> >
<input type="hidden" name="id" value={photo.id} /> <input type="hidden" name="id" value={photo.id} />
<SubmitButtonWithStatus <SubmitButtonWithStatus
icon={<IconGrSync className="translate-y-[-0.5px]" />} icon={<IconGrSync
className="translate-x-[1px] translate-y-[0.5px]"
/>}
onFormSubmitToastMessage={` onFormSubmitToastMessage={`
"${titleForPhoto(photo)}" EXIF data synced "${titleForPhoto(photo)}" EXIF data synced
`} `}

View File

@ -49,7 +49,7 @@ export default function AdminUploadsTable({
'flex flex-nowrap', 'flex flex-nowrap',
'gap-2 sm:gap-3 items-center', 'gap-2 sm:gap-3 items-center',
)}> )}>
<AddButton href={addUploadPath} /> <AddButton path={addUploadPath} />
<FormWithConfirm <FormWithConfirm
action={deleteBlobPhotoAction} action={deleteBlobPhotoAction}
confirmText="Are you sure you want to delete this upload?" confirmText="Are you sure you want to delete this upload?"

View File

@ -11,7 +11,7 @@ export default function ClearCacheButton() {
return ( return (
<form action={syncCacheAction}> <form action={syncCacheAction}>
<SubmitButtonWithStatus <SubmitButtonWithStatus
icon={<BiTrash />} icon={<BiTrash size={16} />}
onFormSubmit={invalidateSwr} onFormSubmit={invalidateSwr}
> >
Clear Cache Clear Cache

View File

@ -14,6 +14,7 @@ export default function DeleteButton (
const { const {
onFormSubmit: onFormSubmitProps, onFormSubmit: onFormSubmitProps,
clearLocalState, clearLocalState,
className,
...rest ...rest
} = props; } = props;
@ -30,11 +31,13 @@ export default function DeleteButton (
return <SubmitButtonWithStatus return <SubmitButtonWithStatus
{...rest} {...rest}
title="Delete" title="Delete"
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />} icon={<BiTrash size={16} />}
spinnerColor="text" spinnerColor="text"
className={clsx( className={clsx(
'text-red-500 dark:text-red-600', className,
'!text-red-500 dark:!text-red-600',
'active:!bg-red-100/50 active:dark:!bg-red-950/50', 'active:!bg-red-100/50 active:dark:!bg-red-950/50',
'disabled:!bg-red-100/50 disabled:dark:!bg-red-950/50',
'!border-red-200 hover:!border-red-300', '!border-red-200 hover:!border-red-300',
'dark:!border-red-900/75 dark:hover:!border-red-900', 'dark:!border-red-900/75 dark:hover:!border-red-900',
)} )}

View File

@ -0,0 +1,54 @@
import Spinner, { SpinnerColor } from '@/components/Spinner';
import { clsx } from 'clsx/lite';
import { ButtonHTMLAttributes, ReactNode } from 'react';
export default function LoaderButton(props: {
children?: ReactNode
isLoading?: boolean
icon?: JSX.Element
spinnerColor?: SpinnerColor
styleAsLink?: boolean
} & ButtonHTMLAttributes<HTMLButtonElement>) {
const {
children,
isLoading,
icon,
spinnerColor,
styleAsLink,
type = 'button',
disabled,
className,
...rest
} = props;
return (
<button
{...rest}
type={type}
className={clsx(
className,
styleAsLink ? 'link h-4' : 'h-9',
'inline-flex items-center gap-2 self-start',
)}
disabled={isLoading || disabled}
>
{(icon || isLoading) &&
<span className={clsx(
'min-w-[1.25rem] h-4 translate-y-[-0.5px]',
'inline-flex justify-center',
)}>
{isLoading
? <Spinner
size={14}
color={spinnerColor}
className="translate-y-[2px]"
/>
: icon}
</span>}
{children && <span className={clsx(
icon !== undefined && 'hidden sm:inline-block',
)}>
{children}
</span>}
</button>
);
}

View File

@ -0,0 +1,66 @@
'use client';
import { useRouter } from 'next/navigation';
import { ReactNode, useEffect, useState, useTransition } from 'react';
import { SpinnerColor } from './Spinner';
import LoaderButton from '@/admin/LoaderButton';
export default function PathLoaderButton({
path,
icon,
prefetch,
loaderDelay = 100,
shouldScroll = true,
shouldReplace,
spinnerColor,
children,
}: {
path: string
icon?: JSX.Element
prefetch?: boolean
loaderDelay?: number
shouldScroll?: boolean
shouldReplace?: boolean
spinnerColor?: SpinnerColor
children?: ReactNode
}) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [shouldShowLoader, setShouldShowLoader] = useState(false);
useEffect(() => {
if (isPending) {
const timeout = setTimeout(() => {
setShouldShowLoader(true);
}, loaderDelay);
return () => clearTimeout(timeout);
} else {
setShouldShowLoader(false);
}
}, [isPending, loaderDelay]);
useEffect(() => {
if (prefetch) {
router.prefetch(path);
}
}, [prefetch, router, path]);
return (
<LoaderButton
icon={icon}
onClick={() => startTransition(() => {
if (shouldReplace) {
router.replace(path, { scroll: shouldScroll });
} else {
router.push(path, { scroll: shouldScroll });
}
})}
isLoading={shouldShowLoader}
spinnerColor={spinnerColor}
>
{children}
</LoaderButton>
);
}

View File

@ -2,9 +2,10 @@
import { HTMLProps, useEffect, useRef } from 'react'; import { HTMLProps, useEffect, useRef } from 'react';
import { useFormStatus } from 'react-dom'; import { useFormStatus } from 'react-dom';
import Spinner, { SpinnerColor } from './Spinner'; import { SpinnerColor } from './Spinner';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { toastSuccess } from '@/toast'; import { toastSuccess } from '@/toast';
import LoaderButton from '@/admin/LoaderButton';
interface Props extends HTMLProps<HTMLButtonElement> { interface Props extends HTMLProps<HTMLButtonElement> {
icon?: JSX.Element icon?: JSX.Element
@ -49,34 +50,21 @@ export default function SubmitButtonWithStatus({
}, [onFormStatusChange, pending]); }, [onFormStatusChange, pending]);
return ( return (
<button <LoaderButton
type="submit" type="submit"
disabled={disabled} disabled={disabled}
className={clsx( className={clsx(
className, className,
'inline-flex items-center gap-2', 'inline-flex items-center gap-2',
primary && 'primary', primary && 'primary',
styleAsLink && 'link',
)} )}
icon={icon}
spinnerColor={spinnerColor}
styleAsLink={styleAsLink}
isLoading={pending}
{...buttonProps} {...buttonProps}
> >
{(icon || pending) && {children}
<span className={clsx( </LoaderButton>
'h-4',
'min-w-[1rem]',
'inline-flex justify-center sm:justify-normal',
'-mx-0.5',
'translate-y-[1px]',
)}>
{pending
? <Spinner size={14} color={spinnerColor} />
: icon}
</span>}
{children && <span className={clsx(
icon !== undefined && 'hidden sm:inline-block',
)}>
{children}
</span>}
</button>
); );
}; };

View File

@ -77,12 +77,13 @@
cursor-pointer cursor-pointer
hover:no-underline hover:no-underline
inline-flex gap-2 items-center inline-flex gap-2 items-center
px-4 px-3
text-base text-base
shadow-sm shadow-sm
active:bg-gray-100 dark:active:bg-gray-900 active:bg-gray-100 dark:active:bg-gray-900
hover:border-gray-300 dark:hover:border-gray-600 hover:border-gray-300 dark:hover:border-gray-600
disabled:cursor-not-allowed disabled:cursor-not-allowed
disabled:text-dim
disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:bg-gray-100 dark:disabled:bg-gray-900
disabled:border-gray-200 disabled:dark:border-gray-700 disabled:border-gray-200 disabled:dark:border-gray-700
} }