Skip to content

Commit

Permalink
feat: first version of ImageUpload component
Browse files Browse the repository at this point in the history
  • Loading branch information
Rileran committed Jun 20, 2022
1 parent 1f41d65 commit eb756df
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 1 deletion.
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

# If you set the following environment variable, you will toggle the request
# behaviour from the mock system Mirage to the URL you will put.
NEXT_PUBLIC_API_BASE_URL =
NEXT_PUBLIC_API_BASE_URL=

# Use the following environment variables to show the environment name.
NEXT_PUBLIC_DEV_ENV_NAME=staging
NEXT_PUBLIC_DEV_ENV_COLOR_SCHEME=teal

# Cloudinary integration
# Make sure that the upload preset supports unsigned upload
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=
27 changes: 27 additions & 0 deletions src/components/ImageUpload/DefaultImagePlaceholder.tsx
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>
);
28 changes: 28 additions & 0 deletions src/components/ImageUpload/cloudinary.service.ts
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;
};
59 changes: 59 additions & 0 deletions src/components/ImageUpload/docs.stories.tsx
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>
);
};
114 changes: 114 additions & 0 deletions src/components/ImageUpload/index.tsx
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>
);
};
3 changes: 3 additions & 0 deletions src/mocks/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const mockServer = () => {

this.namespace = '/';
this.passthrough();

// Enable image upload to cloudinary, even when developing with mocks
this.passthrough('https://api.cloudinary.com/**');
},
});
};

0 comments on commit eb756df

Please sign in to comment.