-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: first version of ImageUpload component
- Loading branch information
Showing
6 changed files
with
237 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { chakra } from '@chakra-ui/react'; | ||
|
||
export const DefaultImagePlaceholder: React.FC = (props) => ( | ||
<chakra.svg width="auto" viewBox="0 0 1600 900" bgColor="#F1F5F9" {...props}> | ||
<g clipPath="url(#a)"> | ||
<path d="M1600 0 0 900M0 0l1600 900" stroke="#CBD5E1" strokeWidth={5} /> | ||
<path fill="#F1F5F9" d="M616 266h368v368H616z" /> | ||
<path | ||
fillRule="evenodd" | ||
clipRule="evenodd" | ||
d="M691.875 327.5c-8.491 0-15.375 6.884-15.375 15.375v215.25c0 8.491 6.884 15.375 15.375 15.375h215.25c8.491 0 15.375-6.884 15.375-15.375v-215.25c0-8.491-6.884-15.375-15.375-15.375h-215.25Zm-46.125 15.375c0-25.474 20.651-46.125 46.125-46.125h215.25c25.474 0 46.125 20.651 46.125 46.125v215.25c0 25.474-20.651 46.125-46.125 46.125h-215.25c-25.474 0-46.125-20.651-46.125-46.125v-215.25Z" | ||
fill="#94A3B8" | ||
/> | ||
<path | ||
fillRule="evenodd" | ||
clipRule="evenodd" | ||
d="M745.688 389a7.688 7.688 0 1 0 0 15.376 7.688 7.688 0 0 0 0-15.376Zm-38.438 7.688c0-21.229 17.209-38.438 38.438-38.438 21.228 0 38.437 17.209 38.437 38.438 0 21.228-17.209 38.437-38.437 38.437-21.229 0-38.438-17.209-38.438-38.437ZM850.128 408.878c6.005-6.004 15.739-6.004 21.744 0l76.875 76.875c6.004 6.005 6.004 15.739 0 21.744-6.005 6.004-15.739 6.004-21.744 0L861 441.494 702.747 599.747c-6.005 6.004-15.739 6.004-21.744 0-6.004-6.004-6.004-15.739 0-21.744l169.125-169.125Z" | ||
fill="#94A3B8" | ||
/> | ||
</g> | ||
<defs> | ||
<clipPath id="a"> | ||
<path fill="#fff" d="M0 0h1600v900H0z" /> | ||
</clipPath> | ||
</defs> | ||
</chakra.svg> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import axios from 'axios'; | ||
|
||
const CLOUDINARY_UPLOAD_ENDPOINT = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload`; | ||
|
||
/** | ||
* Upload an image file to cloudinary using unsigned upload. | ||
* | ||
* See .env.example for cloud name and upload preset configuration | ||
* | ||
* @param file The image to upload to cloudinary | ||
* @returns A URL link to the image on cloudinary | ||
*/ | ||
export const uploadFile = async (file: File): Promise<string> => { | ||
const formData = new FormData(); | ||
|
||
formData.append('file', file); | ||
formData.append( | ||
'upload_preset', | ||
process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET ?? '' | ||
); | ||
|
||
return ( | ||
await axios.post<{ secure_url: string }>( | ||
CLOUDINARY_UPLOAD_ENDPOINT, | ||
formData | ||
) | ||
)?.secure_url; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React, { useState } from 'react'; | ||
|
||
import { Center, Stack, Text } from '@chakra-ui/react'; | ||
import { FiImage } from 'react-icons/fi'; | ||
|
||
import { Icon } from '@/components/Icons'; | ||
import { ImageUpload } from '@/components/ImageUpload'; | ||
|
||
export default { | ||
title: 'Components/ImageUpload', | ||
}; | ||
|
||
export const Default = () => { | ||
const [imageUrl, setImageUrl] = useState<string>(''); | ||
|
||
return ( | ||
<Stack> | ||
<ImageUpload value={imageUrl} onChange={setImageUrl} w="240px" /> | ||
<ImageUpload value={imageUrl} onChange={setImageUrl} w="360px" /> | ||
<ImageUpload | ||
value={imageUrl} | ||
onChange={setImageUrl} | ||
w="480px" | ||
ratio={1} | ||
/> | ||
</Stack> | ||
); | ||
}; | ||
|
||
export const CustomPlaceholder = () => { | ||
const PlaceholderComponent = () => ( | ||
<Center bgColor="gray.50" overflow="hidden"> | ||
<Stack textAlign="center" spacing={2}> | ||
<Icon | ||
fontSize="48px" | ||
textColor="gray.400" | ||
icon={FiImage} | ||
alignSelf="center" | ||
/> | ||
<Text textColor="gray.600" fontWeight="medium" fontSize="md"> | ||
Upload a photo | ||
</Text> | ||
</Stack> | ||
</Center> | ||
); | ||
|
||
const [imageUrl, setImageUrl] = useState<string>(''); | ||
|
||
return ( | ||
<Stack> | ||
<ImageUpload | ||
value={imageUrl} | ||
onChange={setImageUrl} | ||
placeholder={<PlaceholderComponent />} | ||
w="360px" | ||
/> | ||
</Stack> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import React, { useRef, useState } from 'react'; | ||
|
||
import { | ||
AspectRatio, | ||
AspectRatioProps, | ||
Box, | ||
BoxProps, | ||
IconButton, | ||
Image, | ||
ImageProps, | ||
Input, | ||
} from '@chakra-ui/react'; | ||
import { FiX } from 'react-icons/fi'; | ||
|
||
import { Loader } from '@/app/layout'; | ||
import { DefaultImagePlaceholder } from '@/components/ImageUpload/DefaultImagePlaceholder'; | ||
import { uploadFile } from '@/components/ImageUpload/cloudinary.service'; | ||
|
||
export type ImageUploadProps = Omit<BoxProps, 'onChange' | 'placeholder'> & | ||
Pick<AspectRatioProps, 'ratio'> & { | ||
value: string; | ||
onChange: (url: string) => void; | ||
onUploadStateChange?: (isUploading: boolean) => void; | ||
placeholder?: ImageProps['fallback']; | ||
}; | ||
|
||
export const ImageUpload: React.FC<ImageUploadProps> = ({ | ||
value, | ||
onChange, | ||
onUploadStateChange = () => undefined, | ||
placeholder = undefined, | ||
ratio = 16 / 9, | ||
...rest | ||
}) => { | ||
const fileInputRef = useRef<HTMLInputElement>(null); | ||
const [isUploading, setIsUploading] = useState(false); | ||
|
||
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const file = e.target.files?.[0]; | ||
if (!file) { | ||
return; | ||
} | ||
|
||
try { | ||
setIsUploading(true); | ||
onUploadStateChange(true); | ||
|
||
const imageUrl = await uploadFile(file); | ||
console.log({ imageUrl }); | ||
onChange(imageUrl); | ||
} catch (err) { | ||
console.error(err); | ||
} finally { | ||
onUploadStateChange(false); | ||
setIsUploading(false); | ||
} | ||
}; | ||
|
||
const handleDelete = () => { | ||
onChange(''); | ||
if (fileInputRef.current) { | ||
fileInputRef.current.value = ''; | ||
} | ||
}; | ||
|
||
const getFallback = () => { | ||
// If uploading to cloudinary or loading the image from the url, display | ||
// the loader. | ||
if (value || isUploading) { | ||
return <Loader />; | ||
} | ||
|
||
return placeholder ?? <DefaultImagePlaceholder />; | ||
}; | ||
|
||
return ( | ||
<Box | ||
position="relative" | ||
border="1px" | ||
borderStyle="dashed" | ||
borderColor="gray.200" | ||
borderRadius="16px" | ||
overflow="hidden" | ||
cursor="pointer" | ||
transition="border-color 150ms ease-in-out" | ||
_hover={{ borderColor: 'gray.400' }} | ||
{...rest} | ||
> | ||
<Input | ||
ref={fileInputRef} | ||
type="file" | ||
display="none" | ||
onChange={handleChange} | ||
/> | ||
<AspectRatio ratio={ratio} onClick={() => fileInputRef.current?.click()}> | ||
<Image src={value} fallback={getFallback()} /> | ||
</AspectRatio> | ||
{!!value && ( | ||
<IconButton | ||
icon={<FiX />} | ||
position="absolute" | ||
top="0" | ||
right="0" | ||
size="lg" | ||
variant="ghost" | ||
aria-label="Remove photo" | ||
onClick={handleDelete} | ||
_active={{ bgColor: 'blackAlpha.600' }} | ||
_hover={{ bgColor: 'blackAlpha.300' }} | ||
/> | ||
)} | ||
</Box> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters