Refine upload/add experience
This commit is contained in:
parent
53fcdfed94
commit
0460b46f25
23
src/admin/AddButton.tsx
Normal file
23
src/admin/AddButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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] ',
|
||||
|
||||
@ -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?"
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -10,7 +10,7 @@ export default function EditButton ({
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
title="Edit"
|
||||
title={label}
|
||||
href={href}
|
||||
className="button"
|
||||
>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user