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 PathLoaderButton from '@/components/PathLoaderButton';
export default function AddButton ({
href,
label = 'Add',
export default function AddButton({
path,
}: {
href: string,
label?: string,
path: string,
}) {
return (
<Link
title={label}
href={href}
className="button"
<PathLoaderButton
path={path}
icon={<BiImageAdd size={18} className="translate-x-[1px]" />}
>
<BiImageAdd size={18} className="translate-y-[1px]" />
<span className="hidden sm:inline-block">
{label}
</span>
</Link>
Add
</PathLoaderButton>
);
}

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ export default function DeleteButton (
const {
onFormSubmit: onFormSubmitProps,
clearLocalState,
className,
...rest
} = props;
@ -30,11 +31,13 @@ export default function DeleteButton (
return <SubmitButtonWithStatus
{...rest}
title="Delete"
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />}
icon={<BiTrash size={16} />}
spinnerColor="text"
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',
'disabled:!bg-red-100/50 disabled:dark:!bg-red-950/50',
'!border-red-200 hover:!border-red-300',
'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 { useFormStatus } from 'react-dom';
import Spinner, { SpinnerColor } from './Spinner';
import { SpinnerColor } from './Spinner';
import { clsx } from 'clsx/lite';
import { toastSuccess } from '@/toast';
import LoaderButton from '@/admin/LoaderButton';
interface Props extends HTMLProps<HTMLButtonElement> {
icon?: JSX.Element
@ -49,34 +50,21 @@ export default function SubmitButtonWithStatus({
}, [onFormStatusChange, pending]);
return (
<button
<LoaderButton
type="submit"
disabled={disabled}
className={clsx(
className,
'inline-flex items-center gap-2',
primary && 'primary',
styleAsLink && 'link',
)}
icon={icon}
spinnerColor={spinnerColor}
styleAsLink={styleAsLink}
isLoading={pending}
{...buttonProps}
>
{(icon || pending) &&
<span className={clsx(
'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>
{children}
</LoaderButton>
);
};

View File

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