diff --git a/frontend/packages/client/src/App.sass b/frontend/packages/client/src/App.sass index ed71b490e..0e83c86f1 100644 --- a/frontend/packages/client/src/App.sass +++ b/frontend/packages/client/src/App.sass @@ -142,6 +142,8 @@ code border: 1px solid $grey-lighter .border-lighter-dark-grey border: 1px solid $lighter-dark-grey +.border-dashed-dark + border: 2px dashed $lighter-dark-grey .flex-1 flex: 1 0 0 .flex-2 diff --git a/frontend/packages/client/src/components/Community/CommunityEditorProfile.js b/frontend/packages/client/src/components/Community/CommunityEditorProfile.js index d97a513ce..04af315d0 100644 --- a/frontend/packages/client/src/components/Community/CommunityEditorProfile.js +++ b/frontend/packages/client/src/components/Community/CommunityEditorProfile.js @@ -1,15 +1,17 @@ import React, { useState, useEffect, useCallback } from 'react'; +import classnames from 'classnames'; import { useDropzone } from 'react-dropzone'; import { Upload } from 'components/Svg'; import { WrapperResponsive, Loader } from 'components'; import { getReducedImg } from 'utils'; import { useErrorHandlerContext } from 'contexts/ErrorHandler'; -import { MAX_AVATAR_FILE_SIZE } from 'const'; +import { MAX_AVATAR_FILE_SIZE, MAX_FILE_SIZE } from 'const'; function CommunityEditorProfile({ name, body = '', logo, + banner, // fn to update community payload updateCommunity, // fn to upload image @@ -18,15 +20,19 @@ function CommunityEditorProfile({ const [communityName, setCommunityName] = useState(name); const [communityDescription, setCommunityDescription] = useState(body); const [isUpdating, setIsUpdating] = useState(''); + const [isUpdatingImage, setIsUpdatingImage] = useState(false); + const [isUpdatingBanner, setIsUpdatingBanner] = useState(false); const [enableSave, setEnableSave] = useState(false); const [image, setImage] = useState({ imageUrl: logo }); + const [bannerImage, setBannerImage] = useState({ imageUrl: banner }); const { notifyError } = useErrorHandlerContext(); useEffect(() => { if ( (communityName !== name && communityName.length > 0) || communityDescription !== body || - image.file + image.file || + bannerImage.file ) { setEnableSave(true); } @@ -34,34 +40,46 @@ function CommunityEditorProfile({ communityName.trim().length === 0 || (communityName === name && communityDescription === body && - image.file === undefined) + image.file === undefined && + bannerImage.file === undefined) ) { setEnableSave(false); } - }, [name, body, communityName, communityDescription, image]); + }, [name, body, communityName, communityDescription, image, bannerImage]); const saveData = async () => { setIsUpdating(true); + // upload images if any let newImageUrl; - // upload image if any + let newBannerImageUrl; if (image.file) { + setIsUpdatingImage(true); newImageUrl = await uploadFile(image.file); } + if (bannerImage.file) { + setIsUpdatingBanner(true); + newBannerImageUrl = await uploadFile(bannerImage.file); + } const updates = { ...(communityName !== name ? { name: communityName.trim() } : undefined), ...(communityDescription !== body ? { body: communityDescription.trim() } : undefined), ...(newImageUrl?.fileUrl ? { logo: newImageUrl.fileUrl } : undefined), + ...(newBannerImageUrl?.fileUrl + ? { bannerImgUrl: newBannerImageUrl.fileUrl } + : undefined), }; // updated fields if (Object.keys(updates).length > 0) await updateCommunity(updates); setIsUpdating(false); + setIsUpdatingImage(false); + setIsUpdatingBanner(false); setEnableSave(false); }; const onDrop = useCallback( - (acceptedFiles) => { + (filename, dataKey, maxFileSize, maxWidth) => (acceptedFiles) => { acceptedFiles.forEach((imageFile) => { // validate type if ( @@ -75,37 +93,62 @@ function CommunityEditorProfile({ } // validate size if (imageFile.size > MAX_AVATAR_FILE_SIZE) { + const sizeLimit = + maxFileSize === MAX_AVATAR_FILE_SIZE ? '2MB' : '5MB'; notifyError({ status: 'Image file size not allowed', - statusText: 'The selected file exceeds the 2MB limit.', + statusText: `The selected file exceeds the ${sizeLimit} limit.`, }); return; } const imageAsURL = URL.createObjectURL(imageFile); - + const setters = { + logo: setImage, + banner: setBannerImage, + }; const img = new Image(); img.onload = function (e) { - // reduce image if necessary before upload - if (e.target.width > 150) { - getReducedImg(e.target, 150, 'community_image').then((result) => { - setImage({ imageUrl: imageAsURL, file: result.imageFile }); + // reduce images if necessary before upload + if (e.target.width > maxWidth) { + getReducedImg(e.target, maxWidth, filename).then((result) => { + setters[dataKey]({ + imageUrl: imageAsURL, + file: result.imageFile, + }); }); } else { - setImage({ imageUrl: imageAsURL, file: imageFile }); + setters[dataKey]({ imageUrl: imageAsURL, file: imageFile }); } }; img.src = imageAsURL; }); }, - [setImage, notifyError] + [setImage, setBannerImage, notifyError] ); - const { getRootProps, getInputProps } = useDropzone({ - onDrop, + const { getRootProps: getLogoRootProps, getInputProps: getLogoInputProps } = + useDropzone({ + onDrop: onDrop('community_image', 'logo', MAX_AVATAR_FILE_SIZE, 150), + maxFiles: 1, + accept: 'image/jpeg,image/png', + }); + + const { + getRootProps: getBannerRootProps, + getInputProps: getBannerInputProps, + } = useDropzone({ + onDrop: onDrop('community_banner', 'banner', MAX_FILE_SIZE, 1200), maxFiles: 1, accept: 'image/jpeg,image/png', }); + const imageDropClasses = classnames( + 'is-flex is-flex-direction-column is-align-items-center is-justify-content-center cursor-pointer rounded-lg', + { + 'border-dashed-dark': !bannerImage.file && !bannerImage.imageUrl, + } + ); + return ( - {!image && !isUpdating && ( + {!isUpdatingImage && !image?.imageUrl && !image?.file && ( <> Avatar - + )} {image?.imageUrl && ( @@ -164,7 +207,7 @@ function CommunityEditorProfile({ }} /> )} - {!(isUpdating && image.file) ? ( + {!isUpdatingImage && (image?.imageUrl || image?.file) && (
- + +
+ )} + {isUpdatingImage && ( +
+

Uploading...

+
+ )} + + + +
+
+
+ {!isUpdatingBanner && !bannerImage?.imageUrl && ( + <> + + Community Banner Image + + JPG or PNG 200px X 1200px recommended + + + + )} + {bannerImage?.imageUrl && ( +
+ )} + {!isUpdatingBanner && (bannerImage.imageUrl || bannerImage.file) && ( +
+ +
- ) : ( + )} + {isUpdatingBanner && (
{ + (filename, dataKey, maxFileSize, maxWidth) => (acceptedFiles) => { acceptedFiles.forEach((imageFile) => { // validate type if ( @@ -59,10 +60,12 @@ export default function StepOne({ return; } // validate size - if (imageFile.size > MAX_AVATAR_FILE_SIZE) { + if (imageFile.size > maxFileSize) { + const sizeLimit = + maxFileSize === MAX_AVATAR_FILE_SIZE ? '2MB' : '5MB'; notifyError({ status: 'Image file size not allowed', - statusText: 'The selected file exceeds the 2MB limit.', + statusText: `The selected file exceeds the ${sizeLimit} limit.`, }); return; } @@ -70,15 +73,17 @@ export default function StepOne({ const img = new Image(); img.onload = function (e) { - // reduce image if necessary before upload - if (e.target.width > 150) { - getReducedImg(e.target, 150, 'community_image').then((result) => { + // reduce images if necessary before upload + if (e.target.width > maxWidth) { + getReducedImg(e.target, maxWidth, filename).then((result) => { onDataChange({ - logo: { imageUrl: imageAsURL, file: result.imageFile }, + [dataKey]: { imageUrl: imageAsURL, file: result.imageFile }, }); }); } else { - onDataChange({ logo: { imageUrl: imageAsURL, file: imageFile } }); + onDataChange({ + [dataKey]: { imageUrl: imageAsURL, file: imageFile }, + }); } }; img.src = imageAsURL; @@ -87,8 +92,18 @@ export default function StepOne({ [onDataChange, notifyError] ); - const { getRootProps, getInputProps } = useDropzone({ - onDrop, + const { getRootProps: getLogoRootProps, getInputProps: getLogoInputProps } = + useDropzone({ + onDrop: onDrop('community_image', 'logo', MAX_AVATAR_FILE_SIZE, 150), + maxFiles: 1, + accept: 'image/jpeg,image/png', + }); + + const { + getRootProps: getBannerRootProps, + getInputProps: getBannerInputProps, + } = useDropzone({ + onDrop: onDrop('community_banner', 'banner', MAX_FILE_SIZE, 1200), maxFiles: 1, accept: 'image/jpeg,image/png', }); @@ -97,6 +112,7 @@ export default function StepOne({ communityName, communityDescription, logo, + banner, communityTerms, category, } = stepData || {}; @@ -147,6 +163,13 @@ export default function StepOne({ setStepValid(isValid && isCommunityLinksValid); }, [stepData, setStepValid, onDataChange, isCommunityLinksValid]); + const imageDropClasses = classnames( + 'is-flex is-flex-direction-column is-align-items-center is-justify-content-center cursor-pointer rounded-lg', + { + 'border-dashed-dark': !banner?.file, + } + ); + const showNameInputError = !checkValidNameLength(communityName ?? ''); return ( @@ -183,18 +206,11 @@ export default function StepOne({ width: '90px', overflow: 'hidden', position: 'relative', - ...(!logo ? { border: '1px dashed #757575' } : undefined), + ...(!logo ? { border: '2px dashed #757575' } : undefined), }} - {...getRootProps()} + {...getLogoRootProps()} > - {!logo && ( - <> - - Avatar - - - )} - {logo && ( + {logo ? (
+ ) : ( + <> + + Avatar + + )} {logo?.file ? (
- + +
+ ) : null} +
+
+
+
+
+
+ {banner ? ( +
+ ) : ( + <> + + Community Banner Image + + JPG or PNG 200px X 1200px recommended + + + + )} + {banner?.file ? ( +
+ +
) : null}
diff --git a/frontend/packages/client/src/hooks/useCommunity.js b/frontend/packages/client/src/hooks/useCommunity.js index 74b2f1e8d..6162cc09e 100644 --- a/frontend/packages/client/src/hooks/useCommunity.js +++ b/frontend/packages/client/src/hooks/useCommunity.js @@ -94,6 +94,7 @@ export default function useCommunity({ twitterUrl, websiteUrl, logo, + banner, contractAdrress: contractAddr, contractName: contractN, storagePath: storageP, @@ -102,9 +103,10 @@ export default function useCommunity({ strategies, } = communityData; + // check for logo / banner uploads + // admins can edit later the images let communityLogo; - // check for logo upload - // admins can edit later the image + let communityBanner; if (logo?.file) { try { communityLogo = await uploadFile(logo.file); @@ -112,6 +114,13 @@ export default function useCommunity({ communityLogo = undefined; } } + if (banner?.file) { + try { + communityBanner = await uploadFile(banner.file); + } catch (err) { + communityBanner = undefined; + } + } const fetchOptions = { method: 'POST', @@ -133,6 +142,7 @@ export default function useCommunity({ websiteUrl, discordUrl, logo: communityLogo?.fileUrl, + bannerImgUrl: communityBanner?.fileUrl, contractAddress: setDefaultValue( contractAddr, '0x0ae53cb6e3f42a79' diff --git a/frontend/packages/client/src/pages/Community.js b/frontend/packages/client/src/pages/Community.js index 2fc74c4bd..db0a0e7b9 100644 --- a/frontend/packages/client/src/pages/Community.js +++ b/frontend/packages/client/src/pages/Community.js @@ -292,7 +292,16 @@ export default function Community() { return (
{community ? ( -
+
diff --git a/frontend/packages/client/src/pages/CommunityEditor.js b/frontend/packages/client/src/pages/CommunityEditor.js index 048c6de3c..867294a75 100644 --- a/frontend/packages/client/src/pages/CommunityEditor.js +++ b/frontend/packages/client/src/pages/CommunityEditor.js @@ -190,6 +190,7 @@ export default function CommunityEditorPage() { name={community?.name} body={community?.body} logo={community?.logo} + banner={community?.bannerImgUrl} updateCommunity={updateCommunity} uploadFile={uploadFile} />