Chore: Resolves TS errors related to FileReader/ImageUploader (#965)
* Resolves TS errors related to FileReader/ImageUploader * Also tackle cropImage errors Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
parent
a67813ee77
commit
22c4d29db5
4 changed files with 44 additions and 34 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||||
import Cropper from "react-easy-crop";
|
import Cropper from "react-easy-crop";
|
||||||
|
|
||||||
import { Area, getCroppedImg } from "@lib/cropImage";
|
import { Area, getCroppedImg } from "@lib/cropImage";
|
||||||
|
@ -17,6 +17,10 @@ type ImageUploaderProps = {
|
||||||
target: string;
|
target: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface FileEvent<T = Element> extends FormEvent<T> {
|
||||||
|
target: EventTarget & T;
|
||||||
|
}
|
||||||
|
|
||||||
// This is separate to prevent loading the component until file upload
|
// This is separate to prevent loading the component until file upload
|
||||||
function CropContainer({
|
function CropContainer({
|
||||||
onCropComplete,
|
onCropComplete,
|
||||||
|
@ -29,13 +33,13 @@ function CropContainer({
|
||||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
||||||
const [zoom, setZoom] = useState(1);
|
const [zoom, setZoom] = useState(1);
|
||||||
|
|
||||||
const handleZoomSliderChange = ([value]) => {
|
const handleZoomSliderChange = (value: number) => {
|
||||||
value < 1 ? setZoom(1) : setZoom(value);
|
value < 1 ? setZoom(1) : setZoom(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="crop-container max-h-40 h-40 w-40 rounded-full">
|
<div className="w-40 h-40 rounded-full crop-container max-h-40">
|
||||||
<div className="relative h-40 w-40 rounded-full">
|
<div className="relative w-40 h-40 rounded-full">
|
||||||
<Cropper
|
<Cropper
|
||||||
image={imageSrc}
|
image={imageSrc}
|
||||||
crop={crop}
|
crop={crop}
|
||||||
|
@ -77,7 +81,10 @@ export default function ImageUploader({
|
||||||
setImageSrc(props.imageSrc);
|
setImageSrc(props.imageSrc);
|
||||||
}, [props.imageSrc]);
|
}, [props.imageSrc]);
|
||||||
|
|
||||||
const onInputFile = (e) => {
|
const onInputFile = (e: FileEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFile(e.target.files[0]);
|
setFile(e.target.files[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,33 +117,33 @@ export default function ImageUploader({
|
||||||
</div>
|
</div>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="sm:flex sm:items-start mb-4">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<h3 className="font-cal text-lg leading-6 font-bold text-gray-900" id="modal-title">
|
<h3 className="text-lg font-bold leading-6 text-gray-900 font-cal" id="modal-title">
|
||||||
{t("upload_target", { target })}
|
{t("upload_target", { target })}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="cropper mt-6 flex flex-col justify-center items-center p-8 bg-gray-50">
|
<div className="flex flex-col items-center justify-center p-8 mt-6 cropper bg-gray-50">
|
||||||
{!result && (
|
{!result && (
|
||||||
<div className="flex justify-start items-center bg-gray-500 max-h-20 h-20 w-20 rounded-full">
|
<div className="flex items-center justify-start w-20 h-20 bg-gray-500 rounded-full max-h-20">
|
||||||
{!imageSrc && (
|
{!imageSrc && (
|
||||||
<p className="sm:text-xs text-sm text-white w-full text-center">
|
<p className="w-full text-sm text-center text-white sm:text-xs">
|
||||||
{t("no_target", { target })}
|
{t("no_target", { target })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{imageSrc && <img className="h-20 w-20 rounded-full" src={imageSrc} alt={target} />}
|
{imageSrc && <img className="w-20 h-20 rounded-full" src={imageSrc} alt={target} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{result && <CropContainer imageSrc={result} onCropComplete={setCroppedAreaPixels} />}
|
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
||||||
<label className="mt-8 px-3 py-1 text-xs leading-4 font-medium rounded-sm border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 hover:text-gray-900 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900 dark:bg-transparent dark:text-white dark:border-gray-800 dark:hover:bg-gray-900">
|
<label className="px-3 py-1 mt-8 text-xs font-medium leading-4 text-gray-700 bg-white border border-gray-300 rounded-sm hover:bg-gray-50 hover:text-gray-900 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900 dark:bg-transparent dark:text-white dark:border-gray-800 dark:hover:bg-gray-900">
|
||||||
<input
|
<input
|
||||||
onInput={onInputFile}
|
onInput={onInputFile}
|
||||||
type="file"
|
type="file"
|
||||||
name={id}
|
name={id}
|
||||||
placeholder={t("upload_image")}
|
placeholder={t("upload_image")}
|
||||||
className="mt-4 pointer-events-none opacity-0 absolute"
|
className="absolute mt-4 opacity-0 pointer-events-none"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
/>
|
/>
|
||||||
{t("choose_a_file")}
|
{t("choose_a_file")}
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const Slider = ({ value, min, max, step, label, changeHandler }) => (
|
const Slider = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
changeHandler,
|
||||||
|
...props
|
||||||
|
}: Omit<SliderPrimitive.SliderProps, "value"> & {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
changeHandler: (value: number) => void;
|
||||||
|
}) => (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
className="slider mt-2"
|
className="mt-2 slider"
|
||||||
min={min}
|
|
||||||
step={step}
|
|
||||||
max={max}
|
|
||||||
value={[value]}
|
value={[value]}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onValueChange={changeHandler}>
|
onValueChange={(value: number[]) => changeHandler(value[0] ?? value)}
|
||||||
|
{...props}>
|
||||||
<SliderPrimitive.Track className="slider-track">
|
<SliderPrimitive.Track className="slider-track">
|
||||||
<SliderPrimitive.Range className="slider-range" />
|
<SliderPrimitive.Range className="slider-range" />
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
|
|
|
@ -7,7 +7,7 @@ export type Area = {
|
||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createImage = (url) =>
|
const createImage = (url: string) =>
|
||||||
new Promise<HTMLImageElement>((resolve, reject) => {
|
new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.addEventListener("load", () => resolve(image));
|
image.addEventListener("load", () => resolve(image));
|
||||||
|
@ -16,10 +16,11 @@ const createImage = (url) =>
|
||||||
image.src = url;
|
image.src = url;
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function getCroppedImg(imageSrc: string, pixelCrop: Area) {
|
export async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise<string> {
|
||||||
const image = await createImage(imageSrc);
|
const image = await createImage(imageSrc);
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) throw new Error("Context is null, this should never happen.");
|
||||||
|
|
||||||
const maxSize = Math.max(image.naturalWidth, image.naturalHeight);
|
const maxSize = Math.max(image.naturalWidth, image.naturalHeight);
|
||||||
const resizeRatio = MAX_IMAGE_SIZE / maxSize < 1 ? Math.max(MAX_IMAGE_SIZE / maxSize, 0.75) : 1;
|
const resizeRatio = MAX_IMAGE_SIZE / maxSize < 1 ? Math.max(MAX_IMAGE_SIZE / maxSize, 0.75) : 1;
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const useFileReader = (options: UseFileReaderProps) => {
|
||||||
const { method = "readAsText", onLoad } = options;
|
const { method = "readAsText", onLoad } = options;
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<ProgressEvent<FileReader> | null>(null);
|
const [error, setError] = useState<DOMException | null>(null);
|
||||||
const [result, setResult] = useState<string | ArrayBuffer | null>(null);
|
const [result, setResult] = useState<string | ArrayBuffer | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -28,21 +28,16 @@ export const useFileReader = (options: UseFileReaderProps) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadstart = () => setLoading(true);
|
reader.onloadstart = () => setLoading(true);
|
||||||
reader.onloadend = () => setLoading(false);
|
reader.onloadend = () => setLoading(false);
|
||||||
reader.onerror = setError;
|
reader.onerror = () => setError(reader.error);
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
setResult(e.target.result);
|
setResult(e.target?.result ?? null);
|
||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
onLoad(e.target.result);
|
onLoad(e.target?.result ?? null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
reader[method](file);
|
||||||
try {
|
|
||||||
reader[method](file);
|
|
||||||
} catch (e) {
|
|
||||||
setError(e);
|
|
||||||
}
|
|
||||||
}, [file, method, onLoad]);
|
}, [file, method, onLoad]);
|
||||||
|
|
||||||
return [{ result, error, file, loading }, setFile];
|
return [{ result, error, file, loading }, setFile] as const;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue