cal.pub0.org/apps/web/components/ImageUploader.tsx

165 lines
5.2 KiB
TypeScript
Raw Normal View History

import { FormEvent, useCallback, useEffect, useState } from "react";
import Cropper from "react-easy-crop";
2021-10-02 11:04:47 +00:00
import { Area, getCroppedImg } from "@lib/cropImage";
import { useFileReader } from "@lib/hooks/useFileReader";
2021-10-15 10:53:42 +00:00
import { useLocale } from "@lib/hooks/useLocale";
2021-10-02 11:04:47 +00:00
import { DialogClose, DialogTrigger, Dialog, DialogContent } from "@components/Dialog";
import Slider from "@components/Slider";
import Button from "@components/ui/Button";
type ImageUploaderProps = {
id: string;
buttonMsg: string;
handleAvatarChange: (imageSrc: string) => void;
imageSrc?: string;
target: string;
};
interface FileEvent<T = Element> extends FormEvent<T> {
target: EventTarget & T;
}
2021-10-02 11:04:47 +00:00
// This is separate to prevent loading the component until file upload
function CropContainer({
onCropComplete,
imageSrc,
}: {
imageSrc: string;
onCropComplete: (croppedAreaPixels: Area) => void;
}) {
2021-10-15 10:53:42 +00:00
const { t } = useLocale();
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const handleZoomSliderChange = (value: number) => {
2021-08-13 09:34:20 +00:00
value < 1 ? setZoom(1) : setZoom(value);
};
2021-08-13 07:59:48 +00:00
return (
<div className="crop-container h-40 max-h-40 w-40 rounded-full">
<div className="relative h-40 w-40 rounded-full">
2021-10-02 11:04:47 +00:00
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={(croppedArea, croppedAreaPixels) => onCropComplete(croppedAreaPixels)}
onZoomChange={setZoom}
/>
</div>
<Slider
value={zoom}
min={1}
max={3}
step={0.1}
2021-10-15 10:53:42 +00:00
label={t("slide_zoom_drag_instructions")}
2021-10-02 11:04:47 +00:00
changeHandler={handleZoomSliderChange}
/>
</div>
);
}
2021-10-02 11:04:47 +00:00
export default function ImageUploader({
target,
id,
buttonMsg,
handleAvatarChange,
...props
}: ImageUploaderProps) {
2021-10-15 10:53:42 +00:00
const { t } = useLocale();
2021-10-02 11:04:47 +00:00
const [imageSrc, setImageSrc] = useState<string | null>();
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>();
const [{ result }, setFile] = useFileReader({
method: "readAsDataURL",
});
useEffect(() => {
setImageSrc(props.imageSrc);
}, [props.imageSrc]);
const onInputFile = (e: FileEvent<HTMLInputElement>) => {
if (!e.target.files?.length) {
return;
}
2021-10-02 11:04:47 +00:00
setFile(e.target.files[0]);
};
2021-10-02 11:04:47 +00:00
const showCroppedImage = useCallback(
async (croppedAreaPixels) => {
try {
const croppedImage = await getCroppedImg(
result as string /* result is always string when using readAsDataUrl */,
croppedAreaPixels
);
setImageSrc(croppedImage);
handleAvatarChange(croppedImage);
} catch (e) {
console.error(e);
}
},
[result, handleAvatarChange]
);
2021-10-02 11:04:47 +00:00
return (
<Dialog
onOpenChange={
(opened) => !opened && setFile(null) // unset file on close
}>
<DialogTrigger asChild>
Improvement/teams (#1285) * [WIP] checkpoint before pull & merge - Added teams to sidebar - Refactored team settings - Improved team list UI This code will be partly reverted next commit. * [WIP] - Moved team code back to components - Removed team link from sidebar - Built new team manager screen based on Event Type designs - Component-ized frequently reused code (SettingInputContainer, FlatIconButton) * [WIP] - Created LinkIconButton as standalone component - Added functionality to sidebar of team settings - Fixed type bug on public team page induced by my normalization of members array in team query - Removed teams-old which was kept as refrence - Cleaned up loose ends * [WIP] - added create team model - fixed profile missing label due to my removal of default label from component * [WIP] - Fixed TeamCreateModal trigger - removed TeamShell, it didn't make the cut - added getPlaceHolderAvatar - renamed TeamCreate to TeamCreateModal - removed deprecated UsernameInput and replaced uses with suggested TextField * fix save button * [WIP] - Fixed drop down actions on team list - Cleaned up state updates * [WIP] converting teams to tRPC * [WIP] Finished refactor to tRPC * [WIP] Finishing touches * [WIP] Team availability beginning * team availability mvp * - added validation to change role - modified layout of team availability - corrected types * fix ui issue on team availability screen * - added virtualization to team availability - added flexChildrenContainer boolean to Shell to allow for flex on children * availability style fix * removed hard coded team type as teams now use inferred type from tRPC * Removed unneeded vscode settings * Reverted prisma schema * Fixed migrations * Removes unused dayjs plugins * Reverts type regression * Type fix * Type fixes * Type fixes * Moves team availability code to ee Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
2021-12-09 23:51:30 +00:00
<div className="flex items-center">
2021-10-02 11:04:47 +00:00
<Button color="secondary" type="button" className="py-1 text-xs">
{buttonMsg}
</Button>
</div>
</DialogTrigger>
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
2021-10-02 11:04:47 +00:00
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="font-cal text-lg leading-6 text-gray-900" id="modal-title">
2021-10-15 10:53:42 +00:00
{t("upload_target", { target })}
2021-10-02 11:04:47 +00:00
</h3>
</div>
</div>
<div className="mb-4">
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
2021-10-02 11:04:47 +00:00
{!result && (
<div className="flex h-20 max-h-20 w-20 items-center justify-start rounded-full bg-gray-50">
2021-10-15 10:53:42 +00:00
{!imageSrc && (
<p className="w-full text-center text-sm text-white sm:text-xs">
2021-10-15 10:53:42 +00:00
{t("no_target", { target })}
</p>
)}
{imageSrc && <img className="h-20 w-20 rounded-full" src={imageSrc} alt={target} />}
</div>
2021-10-02 11:04:47 +00:00
)}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
2021-10-02 11:04:47 +00:00
<input
onInput={onInputFile}
type="file"
name={id}
2021-10-15 10:53:42 +00:00
placeholder={t("upload_image")}
className="pointer-events-none absolute mt-4 opacity-0"
2021-10-02 11:04:47 +00:00
accept="image/*"
/>
2021-10-15 10:53:42 +00:00
{t("choose_a_file")}
2021-10-02 11:04:47 +00:00
</label>
</div>
</div>
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
2021-10-02 11:04:47 +00:00
<DialogClose asChild>
2021-10-15 10:53:42 +00:00
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
2021-10-02 11:04:47 +00:00
</DialogClose>
<DialogClose asChild>
2021-10-15 10:53:42 +00:00
<Button color="secondary">{t("cancel")}</Button>
2021-10-02 11:04:47 +00:00
</DialogClose>
</div>
</DialogContent>
</Dialog>
);
}