From 7441f1a203b854f632597678721a172462b78089 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Tue, 9 May 2023 03:36:11 +0100 Subject: [PATCH] Refactor avatar implementation --- .env.example | 1 + public/images/default-avatar.png | Bin 0 -> 7300 bytes .../Profile/UploadProfilePicture.tsx | 135 ++++++++++-------- src/components/Setting/UpdateProfileForm.tsx | 20 +-- src/env.d.ts | 1 + 5 files changed, 92 insertions(+), 65 deletions(-) create mode 100644 public/images/default-avatar.png diff --git a/.env.example b/.env.example index 1e1b77ec2..cb12ca496 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ PUBLIC_API_URL=http://api.roadmap.sh +PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars diff --git a/public/images/default-avatar.png b/public/images/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..775eba2fbe787db85b859bfcc1fb9b0510e15c5c GIT binary patch literal 7300 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 => { + const img = new Image(); + + img.onload = () => { + resolve({ width: img.width, height: img.height }); + }; + + img.onerror = () => { + resolve({ width: 0, height: 0 }); + }; + + img.src = URL.createObjectURL(file); + }); +} + +async function validateImage(file: File): Promise { + const dimensions = await getDimensions(file); + + if (dimensions.width > 3000 || dimensions.height > 3000) { + return 'Image dimensions are too big. Maximum 3000x3000 pixels.'; + } + + if (dimensions.width < 100 || dimensions.height < 100) { + return 'Image dimensions are too small. Minimum 100x100 pixels.'; + } + + if (file.size > 1024 * 1024) { + return 'Image size is too big. Maximum 1MB.'; + } + + return null; +} + +export default function UploadProfilePicture(props: UploadProfilePictureProps) { + const { avatarUrl } = props; + const [file, setFile] = useState(null); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); + const inputRef = useRef(null); - const handleFileChange = async (e: Event) => { + const onImageChange = async (e: Event) => { setError(''); + const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; - - // Check file size and dimension - const dimensions = await new Promise<{ - width: number; - height: number; - }>((resolve) => { - const img = new Image(); - img.onload = () => { - resolve({ width: img.width, height: img.height }); - }; - img.src = URL.createObjectURL(file); - }); - - // Image can't be larger than 3000x3000 pixels - if (dimensions.width > 3000 || dimensions.height > 3000) { - setError('Image dimensions are too big. Maximum 3000x3000 pixels.'); - return; - // Image can't be smaller than 100x100 pixels - } else if (dimensions.width < 100 || dimensions.height < 100) { - setError('Image dimensions are too small. Minimum 100x100 pixels.'); + if (!file) { return; } - // Image can't be larger than 1MB - if (file.size > 1024 * 1024) { - setError('Image size is too big. Maximum 1MB.'); + const error = await validateImage(file); + if (error) { + setError(error); return; } - setError(''); setFile( Object.assign(file, { preview: URL.createObjectURL(file), @@ -63,11 +82,16 @@ export default function UploadProfilePicture({ e.preventDefault(); setError(''); setIsLoading(true); - if (!file) return; + + if (!file) { + return; + } const formData = new FormData(); formData.append('name', 'avatar'); formData.append('avatar', file); + + // FIXME: Use `httpCall` helper instead of fetch const res = await fetch( `${import.meta.env.PUBLIC_API_URL}/v1-upload-profile-picture`, { @@ -77,25 +101,29 @@ export default function UploadProfilePicture({ } ); + if (res.ok) { + window.location.reload(); + return; + } + const data = await res.json(); - if (!res.ok) { - setError(data.message || 'Something went wrong'); - setIsLoading(false); - } + setError(data?.message || 'Something went wrong'); + setIsLoading(false); + // Logout user if token is invalid if (data.status === 401) { Cookies.remove(TOKEN_COOKIE_NAME); window.location.reload(); } - - window.location.reload(); }; useEffect(() => { // Necessary to revoke the preview URL when the component unmounts for avoiding memory leaks return () => { - if (file) URL.revokeObjectURL(file.preview); + if (file) { + URL.revokeObjectURL(file.preview); + } }; }, [file]); @@ -105,27 +133,20 @@ export default function UploadProfilePicture({ encType="multipart/form-data" className="mt-8 flex flex-col gap-2" > -