Files
TheGame/packages/web/components/Player/Profile/EditAvatarImage.tsx

150 lines
4.2 KiB
TypeScript

import {
Box,
Button,
Center,
FormControl,
FormErrorMessage,
Image,
Input,
Spinner,
useToast,
} from '@metafam/ds';
import { Maybe, Optional } from '@metafam/utils';
import { forwardRef, useCallback, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import PlayerProfileIcon from '#assets/player-profile-icon.svg';
import { FileReaderData, useImageReader } from '#lib/hooks/useImageReader';
import { optimizedImage } from '#utils/imageHelpers';
export type EditAvatarImageProps = {
initialURL?: Maybe<string>;
onFilePicked: ({ file, dataURL }: FileReaderData) => void;
};
export const EditAvatarImage = forwardRef<
HTMLImageElement,
EditAvatarImageProps
>(({ initialURL, onFilePicked }, ref) => {
const toast = useToast();
const readFile = useImageReader();
const [active, setActive] = useState(false);
const [loading, setLoading] = useState(true);
const [url, setURL] = useState<Optional<string>>(
optimizedImage('profileImageURL', initialURL),
);
const {
control,
formState: { errors },
} = useFormContext();
const onFileChange = useCallback(
async ({ target: input }: { target: HTMLInputElement }) => {
const file = input.files?.[0];
if (!file) return;
if (file.size === 0) {
toast({
title: 'Empty Image Error',
description:
'The selected image has a size of 0. Is it a symbolic link?',
status: 'error',
isClosable: true,
duration: 10000,
});
} else {
setLoading(true);
try {
const dataURL = await readFile(file);
setURL(dataURL);
onFilePicked({ file, dataURL });
} finally {
setLoading(false);
}
}
},
[onFilePicked, readFile, toast],
);
return (
<FormControl isInvalid={!!errors.profileImageURL}>
<Center
w="full"
position="relative"
border="2px solid"
borderColor={active && !url ? 'blue.400' : 'transparent'}
>
<Box h="10em" w="10em" borderRadius="full" display="inline-flex">
<Image
id="profile-image-preview"
ref={ref}
onLoad={() => {
setLoading(false);
}}
display={loading ? 'none' : 'inherit'}
src={url}
borderRadius="full"
objectFit="cover"
h="full"
w="full"
border="2px solid"
borderColor={active && url ? 'blue.400' : 'transparent'}
/>
<Center>
{loading &&
(url == null ? (
<Image maxW="50%" src={PlayerProfileIcon.src} opacity={0.5} />
) : (
<Spinner size="xl" color="purple.500" thickness="4px" />
))}
</Center>
</Box>
<Controller
{...{ control }}
name="profileImageURL"
defaultValue={[]}
render={({ field: { onChange, value, ...props } }) => (
<>
<Button
as="label"
htmlFor="upload-image"
colorScheme="blue"
bg="blue.600"
py={4}
px={8}
letterSpacing="0.1em"
size="md"
fontSize="sm"
ml={3}
color="white"
rounded="md"
_hover={{ bg: 'blue.700' }}
_active={{ bg: 'blue.800' }}
cursor="pointer"
>
Upload Image
<Input
{...props}
id="upload-image"
type="file"
display="none"
onChange={async (evt) => {
onChange(evt.target.files);
onFileChange(evt);
}}
accept="image/*"
onFocus={() => setActive(true)}
onBlur={() => setActive(false)}
/>
</Button>
</>
)}
/>
</Center>
<FormErrorMessage>
{errors.profileImageURL?.message?.toString()}
</FormErrorMessage>
</FormControl>
);
});