Show loading indicator when adding uploads
This commit is contained in:
parent
b431e5de5c
commit
afd0e23a67
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
`}
|
`}
|
||||||
|
|||||||
@ -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?"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
)}
|
)}
|
||||||
|
|||||||
54
src/admin/LoaderButton.tsx
Normal file
54
src/admin/LoaderButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/components/PathLoaderButton.tsx
Normal file
66
src/components/PathLoaderButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user