Vercel/src/components/ImageInput.tsx
2023-10-14 17:01:20 -05:00

136 lines
4.0 KiB
TypeScript

'use client';
import { blobToImage } from '@/utility/blob';
import { useRef, useState } from 'react';
import { CopyExif } from '@/lib/CopyExif';
import { cc } from '@/utility/css';
import { AiOutlineCloudUpload } from 'react-icons/ai';
import Spinner from './Spinner';
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
const INPUT_ID = 'file';
export default function ImageInput({
onStart,
onBlobReady,
maxSize,
quality = 0.8,
loading,
debug,
}: {
onStart?: () => void
onBlobReady?: (blob: Blob, extension?: string) => void
maxSize?: number
quality?: number
loading?: boolean
debug?: boolean
}) {
const ref = useRef<HTMLCanvasElement>(null);
const [fileName, setFileName] = useState<string>();
const [image, setImage] = useState<HTMLImageElement>();
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
<label
htmlFor={INPUT_ID}
className={cc(
'shrink-0 select-none text-main',
loading && 'pointer-events-none cursor-not-allowed',
)}
>
<span
className={cc(
'button primary normal-case',
loading && 'disabled'
)}
aria-disabled={loading}
>
<span className="w-4 inline-flex items-center">
{loading
? <Spinner color="text" />
: <AiOutlineCloudUpload
size={18}
className="translate-y-[0.5px]"
/>}
</span>
Upload Photo
</span>
<input
id={INPUT_ID}
type="file"
className="!hidden"
accept={ACCEPTED_PHOTO_FILE_TYPES.join(',')}
disabled={loading}
onChange={async e => {
onStart?.();
const file = e.currentTarget.files?.[0];
setFileName(file?.name);
const extension = file?.name.split('.').pop()?.toLowerCase();
const canvas = ref.current;
if (file) {
if (maxSize && canvas) {
// 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);
onBlobReady?.(blobWithExif, extension);
}
},
'image/jpeg',
quality,
);
} else {
// No need to process
onBlobReady?.(file);
}
}
}}
/>
</label>
{fileName &&
<div className="max-w-full truncate text-ellipsis">
{fileName}
</div>}
</div>
<canvas
ref={ref}
className={cc(
'bg-gray-50 dark:bg-gray-900/50 rounded-md',
'border border-gray-200 dark:border-gray-800',
'w-[400px]',
(!image || !debug) && 'hidden',
)}
/>
</div>
);
}