Refine upload/add experience

This commit is contained in:
Sam Becker 2023-11-10 16:46:14 -06:00
parent 53fcdfed94
commit 0460b46f25
7 changed files with 102 additions and 73 deletions

23
src/admin/AddButton.tsx Normal file
View File

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

View File

@ -13,7 +13,8 @@ export default function AdminGrid ({
<div className="font-bold">
{title}
</div>}
<div className="min-w-[14rem] overflow-x-scroll">
{/* py-[1px] fixes Safari vertical scroll bug */}
<div className="min-w-[14rem] overflow-x-scroll py-[1px]">
<div className={cc(
'w-full',
'grid grid-cols-[auto_1fr_auto] ',

View File

@ -3,12 +3,12 @@ import AdminGrid from './AdminGrid';
import Link from 'next/link';
import ImageTiny from '@/components/ImageTiny';
import { pathForBlobUrl } from '@/services/blob';
import EditButton from './EditButton';
import FormWithConfirm from '@/components/FormWithConfirm';
import { deleteBlobPhotoAction } from '@/photo/actions';
import DeleteButton from './DeleteButton';
import { cc } from '@/utility/css';
import { pathForAdminUploadUrl } from '@/site/paths';
import AddButton from './AddButton';
export default function BlobUrls({
title,
@ -45,7 +45,7 @@ export default function BlobUrls({
'flex flex-nowrap',
'gap-2 sm:gap-3 items-center',
)}>
<EditButton href={href} label="Setup" />
<AddButton href={href} />
<FormWithConfirm
action={deleteBlobPhotoAction}
confirmText="Are you sure you want to delete this upload?"

View File

@ -6,6 +6,7 @@ export default function DeleteButton () {
return <SubmitButtonWithStatus
title="Delete"
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />}
spinnerColor="text"
className={cc(
'text-red-500 dark:text-red-600',
'active:!bg-red-100/50 active:dark:!bg-red-950/50',

View File

@ -10,7 +10,7 @@ export default function EditButton ({
}) {
return (
<Link
title="Edit"
title={label}
href={href}
className="button"
>

View File

@ -32,8 +32,14 @@ export default function ImageInput({
}) {
const ref = useRef<HTMLCanvasElement>(null);
const [statusText, setStatusText] = useState<string>();
const [image, setImage] = useState<HTMLImageElement>();
const [filesLength, setFilesLength] = useState(0);
const [fileUploadIndex, setFileUploadIndex] = useState(0);
const [fileUploadName, setFileUploadName] = useState('');
const uploadStatusText = filesLength > 1
? `${fileUploadIndex + 1} of ${filesLength}: ${fileUploadName}`
: fileUploadName;
return (
<div className="space-y-4 min-w-0">
@ -60,7 +66,9 @@ export default function ImageInput({
className="translate-y-[0.5px] shrink-0"
/>}
</span>
Upload Photos
{loading
? 'Uploading'
: 'Upload Photos'}
</span>
<input
id={INPUT_ID}
@ -73,79 +81,73 @@ export default function ImageInput({
onStart?.();
const { files } = e.currentTarget;
if (files && files.length > 0) {
for (let i = 0; i < files?.length; i++) {
setFilesLength(files.length);
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
const callbackArgs = {
extension: file.name.split('.').pop()?.toLowerCase(),
hasMultipleUploads: files.length > 1,
isLastBlob: i === files.length - 1,
};
if (files.length > 1) {
setStatusText(
`Uploading ${i + 1} of ${files.length}: ${file.name}`
);
} else {
setStatusText(`Uploading ${file.name}`);
}
const canvas = ref.current;
if (!(maxSize && canvas)) {
// No need to process
await onBlobReady?.({
...callbackArgs,
blob: file,
});
} else {
// Process images that need resizing
const image = await blobToImage(file);
setImage(image);
const { naturalWidth, naturalHeight } = image;
const ratio = naturalWidth / naturalHeight;
const width =
Math.round(ratio >= 1 ? maxSize : maxSize * ratio);
const height =
Math.round(ratio >= 1 ? maxSize / ratio : maxSize);
canvas.width = width;
canvas.height = height;
// Specify wide gamut to avoid data loss while resizing
const ctx = canvas.getContext(
'2d',
{ colorSpace: 'display-p3' },
);
ctx?.drawImage(
image,
0,
0,
canvas.width,
canvas.height,
);
canvas.toBlob(
async blob => {
if (blob) {
const blobWithExif = await CopyExif(file, blob);
await onBlobReady?.({
...callbackArgs,
blob: blobWithExif,
});
}
},
'image/jpeg',
quality,
);
}
setFileUploadIndex(i);
setFileUploadName(file.name);
const callbackArgs = {
extension: file.name.split('.').pop()?.toLowerCase(),
hasMultipleUploads: files.length > 1,
isLastBlob: i === files.length - 1,
};
const canvas = ref.current;
if (!(maxSize && canvas)) {
// No need to process
await onBlobReady?.({
...callbackArgs,
blob: file,
});
} else {
// Process images that need resizing
const image = await blobToImage(file);
setImage(image);
const { naturalWidth, naturalHeight } = image;
const ratio = naturalWidth / naturalHeight;
const width =
Math.round(ratio >= 1 ? maxSize : maxSize * ratio);
const height =
Math.round(ratio >= 1 ? maxSize / ratio : maxSize);
canvas.width = width;
canvas.height = height;
// Specify wide gamut to avoid data loss while resizing
const ctx = canvas.getContext(
'2d',
{ colorSpace: 'display-p3' },
);
ctx?.drawImage(
image,
0,
0,
canvas.width,
canvas.height,
);
canvas.toBlob(
async blob => {
if (blob) {
const blobWithExif = await CopyExif(file, blob);
await onBlobReady?.({
...callbackArgs,
blob: blobWithExif,
});
}
},
'image/jpeg',
quality,
);
}
}
}
}}
/>
</label>
{statusText &&
{filesLength > 0 &&
<div className="max-w-full truncate text-ellipsis">
{statusText}
{uploadStatusText}
</div>}
</div>
<canvas

View File

@ -2,18 +2,20 @@
import { HTMLProps } from 'react';
import { useFormStatus } from 'react-dom';
import Spinner from './Spinner';
import Spinner, { SpinnerColor } from './Spinner';
import { cc } from '@/utility/css';
interface Props extends HTMLProps<HTMLButtonElement> {
icon?: JSX.Element
styleAsLink?: boolean
spinnerColor?: SpinnerColor
}
export default function SubmitButtonWithStatus(props: Props) {
const {
icon,
styleAsLink,
spinnerColor,
children,
disabled,
className,
@ -43,7 +45,7 @@ export default function SubmitButtonWithStatus(props: Props) {
'translate-y-[1px]',
)}>
{pending
? <Spinner size={14} />
? <Spinner size={14} color={spinnerColor} />
: icon}
</span>}
{children && <span className={cc(