diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 3f4dc178..95836056 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -23,6 +23,8 @@ import { Colors } from './utils/colors'; import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; import { useCreateSubgraphApolloClient, useCreateMongoDbApolloClient } from './hooks/apollo'; import { MongoDbApolloProvider } from './components/providers/MongoDbApolloProvider'; +import CreateGoodCollectivePage from './pages/CreateGoodCollectivePage'; +import { CreatePoolProvider } from './hooks/useCreatePool'; const queryClient = new QueryClient(); const projectId = 'b1b7664bfba2f6ad5538aa7fa9a2404f'; @@ -67,6 +69,14 @@ function App(): JSX.Element { } /> } /> + + + + } + /> } /> } /> } /> diff --git a/packages/app/src/assets/CommunityFundsIcon.png b/packages/app/src/assets/CommunityFundsIcon.png new file mode 100644 index 00000000..5b690550 Binary files /dev/null and b/packages/app/src/assets/CommunityFundsIcon.png differ diff --git a/packages/app/src/assets/CreateCollectiveLogo.png b/packages/app/src/assets/CreateCollectiveLogo.png new file mode 100644 index 00000000..e29e1856 Binary files /dev/null and b/packages/app/src/assets/CreateCollectiveLogo.png differ diff --git a/packages/app/src/assets/DefaultIcon.svg b/packages/app/src/assets/DefaultIcon.svg new file mode 100644 index 00000000..11c40919 --- /dev/null +++ b/packages/app/src/assets/DefaultIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/DiscordIcon.svg b/packages/app/src/assets/DiscordIcon.svg new file mode 100644 index 00000000..fb59dbce --- /dev/null +++ b/packages/app/src/assets/DiscordIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/EditIcon.svg b/packages/app/src/assets/EditIcon.svg new file mode 100644 index 00000000..831b848c --- /dev/null +++ b/packages/app/src/assets/EditIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/Mask.png b/packages/app/src/assets/Mask.png new file mode 100644 index 00000000..8084bf32 Binary files /dev/null and b/packages/app/src/assets/Mask.png differ diff --git a/packages/app/src/assets/ResultsBasedIcon.png b/packages/app/src/assets/ResultsBasedIcon.png new file mode 100644 index 00000000..6baa2256 Binary files /dev/null and b/packages/app/src/assets/ResultsBasedIcon.png differ diff --git a/packages/app/src/assets/RocketLaunchIcon.svg b/packages/app/src/assets/RocketLaunchIcon.svg new file mode 100644 index 00000000..af1a4048 --- /dev/null +++ b/packages/app/src/assets/RocketLaunchIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/SegmentedAidIcon.png b/packages/app/src/assets/SegmentedAidIcon.png new file mode 100644 index 00000000..91ab2970 Binary files /dev/null and b/packages/app/src/assets/SegmentedAidIcon.png differ diff --git a/packages/app/src/assets/SettingsIcon.svg b/packages/app/src/assets/SettingsIcon.svg new file mode 100644 index 00000000..9dc45ad4 --- /dev/null +++ b/packages/app/src/assets/SettingsIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/assets/SuccessGuyIcon.svg b/packages/app/src/assets/SuccessGuyIcon.svg new file mode 100644 index 00000000..28a58325 --- /dev/null +++ b/packages/app/src/assets/SuccessGuyIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/app/src/assets/SuccessIcon.png b/packages/app/src/assets/SuccessIcon.png new file mode 100644 index 00000000..5291469c Binary files /dev/null and b/packages/app/src/assets/SuccessIcon.png differ diff --git a/packages/app/src/assets/ThreadsIcon.png b/packages/app/src/assets/ThreadsIcon.png new file mode 100644 index 00000000..10cf07ec Binary files /dev/null and b/packages/app/src/assets/ThreadsIcon.png differ diff --git a/packages/app/src/assets/UploadIcon.png b/packages/app/src/assets/UploadIcon.png new file mode 100644 index 00000000..a6ac633d Binary files /dev/null and b/packages/app/src/assets/UploadIcon.png differ diff --git a/packages/app/src/assets/WebsiteIcon.svg b/packages/app/src/assets/WebsiteIcon.svg new file mode 100644 index 00000000..fc84fe77 --- /dev/null +++ b/packages/app/src/assets/WebsiteIcon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/index.ts b/packages/app/src/assets/index.ts index 50440432..f1e0ec8c 100644 --- a/packages/app/src/assets/index.ts +++ b/packages/app/src/assets/index.ts @@ -54,3 +54,17 @@ export { default as Woman } from './Woman.svg'; export { default as chevronDown } from './chevron-down.svg'; export { default as chevronRight } from './chevron-right.svg'; export { default as empty } from './empty.svg'; +export { default as CommunityFundsIcon } from './CommunityFundsIcon.png'; +export { default as ResultsBasedIcon } from './ResultsBasedIcon.png'; +export { default as SegmentedAidIcon } from './SegmentedAidIcon.png'; +export { default as CreateCollectiveLogo } from './CreateCollectiveLogo.png'; +export { default as UploadIcon } from './UploadIcon.png'; +export { default as RocketLaunchIcon } from './RocketLaunchIcon.svg'; +export { default as EditIcon } from './EditIcon.svg'; +export { default as Mask } from './Mask.png'; +export { default as SuccessIcon } from './SuccessIcon.png'; +export { default as SuccessGuyIcon } from './SuccessGuyIcon.svg'; +export { default as SettingsIcon } from './SettingsIcon.svg'; +export { default as DefaultIcon } from './DefaultIcon.svg'; +export { default as DiscordIcon } from './DiscordIcon.svg'; +export { default as WebsiteIcon } from './WebsiteIcon.svg'; diff --git a/packages/app/src/components/ActionButton.tsx b/packages/app/src/components/ActionButton.tsx index 5a257001..ac2f8f4a 100644 --- a/packages/app/src/components/ActionButton.tsx +++ b/packages/app/src/components/ActionButton.tsx @@ -1,13 +1,15 @@ import { Box, Link, Pressable, Text, useBreakpointValue } from 'native-base'; +import { ReactNode } from 'react'; import { InterSemiBold } from '../utils/webFonts'; type ActionButtonProps = { href?: string; - text: string; + text: string | ReactNode; bg: string; textColor: string; onPress?: any; + width?: string; }; export const buttonStyles = { @@ -38,7 +40,7 @@ export const buttonStyles = { }, }; -const ActionButton = ({ href, text, bg, textColor, onPress }: ActionButtonProps) => { +const ActionButton = ({ href, text, bg, textColor, onPress, width = '100%' }: ActionButtonProps) => { const responsiveStyles = useBreakpointValue({ base: { button: { @@ -53,7 +55,7 @@ const ActionButton = ({ href, text, bg, textColor, onPress }: ActionButtonProps) }, buttonContainer: { ...buttonStyles.buttonContainer, - width: '100%', + width, }, }, lg: buttonStyles, diff --git a/packages/app/src/components/FileUpload.tsx b/packages/app/src/components/FileUpload.tsx new file mode 100644 index 00000000..84cf36c4 --- /dev/null +++ b/packages/app/src/components/FileUpload.tsx @@ -0,0 +1,43 @@ +import { useRef, useState } from 'react'; +import { Box, Center, Pressable, Text } from 'native-base'; + +import { UploadIcon } from '../assets'; + +const FileUpload = ({ style, onUpload }: { style: Object | {}; onUpload: Function }) => { + const uploader = useRef(null); + const [fileName, setFileName] = useState(''); + + return ( + (uploader.current as unknown as HTMLInputElement)?.click()}> + +
+ + {!fileName && Click to upload} + {fileName} +
+
+ { + if (!event.target.files) return; + setFileName(event.target.files[0].name); + onUpload(event.target.files[0]); + }} + /> +
+ ); +}; + +export default FileUpload; diff --git a/packages/app/src/components/WarningBox.tsx b/packages/app/src/components/WarningBox.tsx new file mode 100644 index 00000000..430ba574 --- /dev/null +++ b/packages/app/src/components/WarningBox.tsx @@ -0,0 +1,45 @@ +import { Text, Image, HStack, Link, VStack } from 'native-base'; +import { InfoIconOrange } from '../assets'; + +const WarningBox = ({ content, explanationProps = {} }: any) => { + const Explanation = content.Explanation; + + return ( + + + + + + {content.title} + + + {Explanation ? : null} + + {content.suggestion ? ( + + + You may: + + + + {content.suggestion.map((suggestion: string, index: number) => ( + + {index + 1}. {suggestion} + + ))} + {content.href && ( + + {content.suggestion.length + 1}. + Purchase and use GoodDollar + + )} + + + + ) : null} + + + ); +}; + +export default WarningBox; diff --git a/packages/app/src/components/create/CreateContract/1GetStarted.tsx b/packages/app/src/components/create/CreateContract/1GetStarted.tsx new file mode 100644 index 00000000..8087e540 --- /dev/null +++ b/packages/app/src/components/create/CreateContract/1GetStarted.tsx @@ -0,0 +1,468 @@ +import { useEffect, useState } from 'react'; +import { + VStack, + Text, + FormControl, + Input, + WarningOutlineIcon, + TextArea, + HStack, + Box, + Flex, + ChevronLeftIcon, + WarningTwoIcon, + ArrowForwardIcon, + Button, +} from 'native-base'; +import { StyleSheet } from 'react-native'; + +import ActionButton from '../../ActionButton'; +import { useScreenSize } from '../../../theme/hooks'; +import { useCreatePool } from '../../../hooks/useCreatePool/useCreatePool'; + +const Warning = ({ width }: { width: string }) => { + return ( + + + + Please fill all required fields before proceedings to details section + + + ); +}; + +type FormError = { + projectName?: string; + projectDescription?: string; + tagline?: string; + logo?: string; + coverPhoto?: string; +}; + +const GetStarted = ({}: {}) => { + const { form, nextStep, previousStep, submitPartial } = useCreatePool(); + + const { isDesktopView } = useScreenSize(); + + const [projectName, setProjectName] = useState(form.projectName ?? ''); + const [tagline, setTagline] = useState(form.tagline ?? ''); + const [rewardDescription, setRewardDescription] = useState(form.rewardDescription ?? ''); + const [projectDescription, setProjectDescription] = useState(form.projectDescription ?? ''); + const [logo, setLogo] = useState(form.logo ?? ''); + const [coverPhoto, setCoverPhoto] = useState(form.coverPhoto ?? ''); + const [errors, setErrors] = useState({}); + const [showWarning, setShowWarning] = useState(false); + const [hasErrors, setHasErrors] = useState(false); + const [logoInput, setLogoInput] = useState(); + const [coverPhotoInput, setCoverPhotoInput] = useState(); + const [isValidating, setIsValidating] = useState(false); + + const submitForm = () => { + // Only show warning after the form has been submitted + setShowWarning(true); + if (validate(true)) { + submitPartial({ + projectName, + tagline, + projectDescription, + logo, + coverPhoto, + }); + nextStep(); + } + }; + + const validate = (checkEmpty = false) => { + setIsValidating(true); + const currErrors: FormError = { + projectName: '', + projectDescription: '', + tagline: '', + logo: '', + coverPhoto: '', + }; + let pass = true; + + // Pool Name* - 100 character max + // (Can have spaces, no special characters allowed, 0-9/a-z/A-Z) + if (!projectName) { + if (checkEmpty) { + currErrors.projectName = 'Project name is required'; + pass = false; + } + } else if (projectName.length > 30) { + currErrors.projectName = 'Project name length (max 30 characters)'; + pass = false; + } else if (!/^[a-zA-Z0-9]*$/.test(projectName)) { + currErrors.projectName = 'Project name cannot contain special characters'; + pass = false; + } + + // Pool Description* - 500 character max + if (!projectDescription) { + if (checkEmpty) { + currErrors.projectDescription = 'Project description is required'; + pass = false; + } + } else if (projectDescription.length > 500) { + currErrors.projectDescription = 'Project description length (max 500 characteres)'; + pass = false; + } + + if (!logo) { + if (checkEmpty) { + currErrors.logo = 'Logo is required'; + pass = false; + } + } + + setErrors({ + ...errors, + ...currErrors, + }); + setIsValidating(false); + + return pass; + }; + + const validateImg = async ( + imgUrl: string, + maxSize: number, + maxWidth: number, + maxHeight: number, + imgType: 'logo' | 'coverPhoto' + ) => { + const response = await fetch(imgUrl, { method: 'HEAD' }); + const contentLength = response.headers.get('content-length'); + const size = contentLength ? parseInt(contentLength, 10) : null; + if (!size) throw new Error('Error: Image size 0'); + if (size > maxSize * 1024 * 1024) { + throw new Error("'Image size (max ${maxSize} MB)'"); + } + + const img = new Image(); + img.onload = function () { + if (img.width > maxWidth || img.height > maxHeight) { + console.log( + `${imgType === 'logo' ? 'Logo' : 'Cover Photo'} height(${img.height}), width(${img.width}) exceedes limit!` + ); + setErrors({ + ...errors, + [imgType]: `${imgType === 'logo' ? 'Logo' : 'Cover Photo'} height(${img.height}), width(${ + img.width + }) exceedes limit!`, + }); + if (imgType === 'logo') { + setLogo(''); + } else setCoverPhoto(''); + } + }; + img.src = imgUrl; + if (img.complete && img.naturalWidth !== 0) { + console.log(img); + } + }; + + const onSubmitLogo = async () => { + try { + validateImg(logoInput!, 1, 500, 500, 'logo'); + setLogo(logoInput!); + } catch (error: Error | any) { + setErrors({ + ...errors, + logo: error.message, + }); + } + }; + + const onSubmitCoverPhoto = async () => { + try { + validateImg(coverPhotoInput!, 20, 1400, 256, 'coverPhoto'); + setCoverPhoto(coverPhotoInput!); + } catch (error: Error | any) { + setErrors({ + ...errors, + coverPhoto: error.message, + }); + } + }; + + useEffect(() => { + setHasErrors(Object.values(errors).filter((value) => value).length !== 0); + }, [errors]); + + return ( + + + Get Started + + + Add basic information about your project, details can be edited later + + + + + Project Name + + + {isDesktopView && ( + + Give a brief name to your project that it can be identified with. + + )} + setProjectName(val)} + onBlur={() => validate()} + autoComplete={undefined} + borderRadius={8} + /> + {errors.projectName && ( + + + + {errors.projectName} + + + )} + + + + + Tagline + + + setTagline(val)} + onBlur={() => validate()} + borderRadius={8} + /> + {errors.tagline && ( + + + + Something is wrong. + + + )} + + + + + Reward Description + + + setRewardDescription(val)} + onBlur={() => validate()} + borderRadius={8} + /> + {errors.tagline && ( + + + + Something is wrong. + + + )} + + + + + Project Description + + +