mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-01-14 17:08:00 -05:00
150 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
});
|