From 7ef054d5b0f6097427a38af204e658fa877fa42c Mon Sep 17 00:00:00 2001 From: J Francisco Rader Date: Tue, 9 May 2023 01:19:33 -0300 Subject: [PATCH 1/4] feat: add profile to comment box --- src/context/auth.tsx | 3 + src/helpers/useBTCConverter.ts | 29 ++- src/hooks/useFundingFormState.tsx | 164 +++++++------- .../components/FundingFormSection.tsx | 10 +- .../projectActivityPanel/index.tsx | 39 ++-- .../ProjectFundingSelectionFormScreen.tsx | 21 +- .../components/FundingFormRewardItem.tsx | 4 +- .../ProjectFundingFormCommentField.tsx | 207 ++++++++++++++++++ .../ProjectPaymentFormFundingComment.tsx | 175 --------------- .../projectMainBody/components/index.ts | 2 +- 10 files changed, 353 insertions(+), 301 deletions(-) create mode 100644 src/pages/projectView/projectMainBody/components/ProjectFundingFormCommentField.tsx delete mode 100644 src/pages/projectView/projectMainBody/components/ProjectPaymentFormFundingComment.tsx diff --git a/src/context/auth.tsx b/src/context/auth.tsx index cac035fb1..3005d8a7b 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -17,6 +17,7 @@ import { Project, User } from '../types/generated/graphql' const defaultContext: AuthContextProps = { isLoggedIn: false, user: defaultUser, + isAnonymous: true, loading: false, error: undefined, login() {}, @@ -44,6 +45,7 @@ export type NavContextProps = { type AuthContextProps = { isLoggedIn: boolean user: User + isAnonymous: boolean loading: boolean error?: ApolloError login: (me: User) => void @@ -162,6 +164,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { { const { btcRate } = useBtcContext() - const getUSDAmount = (satoshis: Satoshis): USDollars => { - return (satoshis * btcRate) as USDollars - } + const getUSDAmount = useCallback( + (satoshis: Satoshis): USDollars => { + return (satoshis * btcRate) as USDollars + }, + [btcRate], + ) - const getUSDCentsAmount = (satoshis: Satoshis): USDCents => { - return (satoshis * btcRate * 100) as USDCents - } + const getUSDCentsAmount = useCallback( + (satoshis: Satoshis): USDCents => { + return (satoshis * btcRate * 100) as USDCents + }, + [btcRate], + ) - const getSatoshisFromUSDCents = (usdCents: USDCents): Satoshis => { - return Math.round(usdCents / 100 / btcRate) as Satoshis - } + const getSatoshisFromUSDCents = useCallback( + (usdCents: USDCents): Satoshis => { + return Math.round(usdCents / 100 / btcRate) as Satoshis + }, + [btcRate], + ) return { getUSDAmount, diff --git a/src/hooks/useFundingFormState.tsx b/src/hooks/useFundingFormState.tsx index 6e13c89e4..42f5a5dcc 100644 --- a/src/hooks/useFundingFormState.tsx +++ b/src/hooks/useFundingFormState.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { AuthContext } from '../context' import { useBTCConverter } from '../helpers' @@ -45,28 +45,38 @@ export const useFundingFormState = ({ rewards }: UseFundStateProps) => { const { user } = useContext(AuthContext) const { getUSDCentsAmount } = useBTCConverter() - const initialState: IFundForm = { - donationAmount: 0, - rewardsCost: 0, - comment: '', - shippingDestination: ShippingDestination.National, - shippingCost: 0, - anonymous: !(user && user.id), // The default user has id 0 - funderAvatarURL: user.imageUrl || '', - funderUsername: user.username, - email: '', - media: '', - rewardsByIDAndCount: undefined, - rewardCurrency: RewardCurrency.Usdcent, - } + const initialState: IFundForm = useMemo( + () => ({ + donationAmount: 0, + rewardsCost: 0, + comment: '', + shippingDestination: ShippingDestination.National, + shippingCost: 0, + anonymous: !(user && user.id), // The default user has id 0 + funderAvatarURL: user.imageUrl || '', + funderUsername: user.username, + email: '', + media: '', + rewardsByIDAndCount: undefined, + rewardCurrency: RewardCurrency.Usdcent, + }), + [user], + ) const [state, _setState] = useState(initialState) - const setTarget = (event: any) => { - const { name, value } = event.target - const newState = { ...state, [name]: value } - _setState(newState) - } + const setTarget = useCallback( + (event: any) => { + const { name, value } = event.target + const newState = { ...state, [name]: value } + _setState(newState) + }, + [state], + ) + + const setState = useCallback((name: string, value: any) => { + _setState((current) => ({ ...current, [name]: value })) + }, []) useEffect(() => { if (!user || !user.id) { @@ -74,64 +84,66 @@ export const useFundingFormState = ({ rewards }: UseFundStateProps) => { } else { setState('anonymous', false) } - }, [user]) - - const setState = (name: string, value: any) => { - const newState = { ...state, [name]: value } - _setState(newState) - } - - const updateReward = ({ id, count }: IRewardCount) => { - const newRewardsCountInfo = { ...state.rewardsByIDAndCount } - - if (count !== 0) { - newRewardsCountInfo[id as unknown as keyof ProjectReward] = count - } else if (count === 0) { - delete newRewardsCountInfo[id as unknown as keyof ProjectReward] - } - - let rewardsCost = 0 - - if (rewards) { - Object.keys(newRewardsCountInfo).forEach((rewardID: string) => { - const id = parseInt(rewardID, 10) - - const reward = rewards.find( - (reward: ProjectReward) => - reward.id === id || `${reward.id}` === rewardID, - ) - - if (reward && reward.id) { - const rewardMultiplier = - newRewardsCountInfo[rewardID as keyof ProjectReward] - if (!rewardMultiplier) { - return 0 + }, [setState, user]) + + const updateReward = useCallback( + ({ id, count }: IRewardCount) => { + const newRewardsCountInfo = { ...state.rewardsByIDAndCount } + + if (count !== 0) { + newRewardsCountInfo[id as unknown as keyof ProjectReward] = count + } else if (count === 0) { + delete newRewardsCountInfo[id as unknown as keyof ProjectReward] + } + + let rewardsCost = 0 + + if (rewards) { + Object.keys(newRewardsCountInfo).forEach((rewardID: string) => { + const id = parseInt(rewardID, 10) + + const reward = rewards.find( + (reward: ProjectReward) => + reward.id === id || `${reward.id}` === rewardID, + ) + + if (reward && reward.id) { + const rewardMultiplier = + newRewardsCountInfo[rewardID as keyof ProjectReward] + if (!rewardMultiplier) { + return 0 + } + + const cost = + state.rewardCurrency === RewardCurrency.Usdcent + ? reward.cost + : // Assume sats if not USD cents + getUSDCentsAmount(reward.cost as Satoshis) + + rewardsCost += cost * rewardMultiplier } - - const cost = - state.rewardCurrency === RewardCurrency.Usdcent - ? reward.cost - : // Assume sats if not USD cents - getUSDCentsAmount(reward.cost as Satoshis) - - rewardsCost += cost * rewardMultiplier - } - }) - } - - const newState = { - ...state, - rewardsByIDAndCount: newRewardsCountInfo, - rewardsCost, - totalAmount: rewardsCost + state.donationAmount, - } - - _setState(newState) - } - - const resetForm = () => { + }) + } + + _setState((current) => ({ + ...current, + rewardsByIDAndCount: newRewardsCountInfo, + rewardsCost, + totalAmount: rewardsCost + state.donationAmount, + })) + }, + [ + getUSDCentsAmount, + rewards, + state.donationAmount, + state.rewardCurrency, + state.rewardsByIDAndCount, + ], + ) + + const resetForm = useCallback(() => { _setState(initialState) - } + }, [initialState]) return { state, setTarget, setState, updateReward, resetForm } } diff --git a/src/pages/projectView/projectActivityPanel/components/FundingFormSection.tsx b/src/pages/projectView/projectActivityPanel/components/FundingFormSection.tsx index 46d905cd5..4014f61a0 100644 --- a/src/pages/projectView/projectActivityPanel/components/FundingFormSection.tsx +++ b/src/pages/projectView/projectActivityPanel/components/FundingFormSection.tsx @@ -7,13 +7,13 @@ import { SectionTitle } from '../../../../components/ui' import { IFundForm } from '../../../../hooks' import { IRewardCount } from '../../../../interfaces' import { colors } from '../../../../styles' -import { ProjectReward } from '../../../../types/generated/graphql' +import { ProjectRewardForCreateUpdateFragment } from '../../../../types/generated/graphql' import { FundingFormRewardItem } from '../../projectMainBody/components/FundingFormRewardItem' type Props = { setFormState: any updateReward: (_: IRewardCount) => void - rewards?: ProjectReward[] + rewards?: ProjectRewardForCreateUpdateFragment[] formState?: IFundForm } @@ -25,7 +25,9 @@ export const FundingFormSection = ({ }: Props) => { const getRewardCount = (rewardId: number) => formState?.rewardsByIDAndCount - ? formState?.rewardsByIDAndCount[`${rewardId}` as keyof ProjectReward] + ? formState?.rewardsByIDAndCount[ + `${rewardId}` as keyof ProjectRewardForCreateUpdateFragment + ] : 0 const hasRewards = rewards && rewards.length @@ -82,7 +84,7 @@ export const FundingFormSection = ({ /> - {rewards.map((reward: ProjectReward) => ( + {rewards.map((reward) => ( { if (user && user.id) { setFormState('anonymous', false) } - }, [user]) + }, [setFormState, user]) useEffect(() => { if (!formState.anonymous && (!user || !user.id)) { loginOnOpen() setFormState('anonymous', true) } - }, [formState.anonymous]) + }, [formState.anonymous, loginOnOpen, setFormState, user]) const handleCloseButton = () => { setMobileView(MobileViews.contribution) @@ -168,10 +169,6 @@ export const ProjectActivityPanel = ({ requestFunding(input) } - const getActivityHeight = () => { - return 'calc(100% - 20px)' - } - const renderPanelContent = () => { if (!project || !project.id) { return @@ -192,21 +189,17 @@ export const ProjectActivityPanel = ({ case fundingStages.form: return ( reward !== null, - ) as ProjectReward[], - type: project.type, - name: project.name, - }} + fundingRequestLoading={fundingRequestLoading} + isMobile={isMobile} + handleCloseButton={handleCloseButton} + formState={formState} + setFormState={setFormState} + setTarget={setTarget} + updateReward={updateReward} + handleFund={handleFund} + rewards={project.rewards?.filter(truthyFilter)} + type={project.type} + name={project.name} /> ) case fundingStages.started: @@ -245,7 +238,7 @@ export const ProjectActivityPanel = ({ alignItems="center" backgroundColor="#FFFFFF" marginTop={isMobile ? '0px' : '20px'} - height={getActivityHeight()} + height="calc(100% - 20px)" borderTopLeftRadius={isMobile ? 'initial' : '8px'} overflow="hidden" borderTop={isMobile ? 'none' : '2px solid'} diff --git a/src/pages/projectView/projectActivityPanel/screens/ProjectFundingSelectionFormScreen.tsx b/src/pages/projectView/projectActivityPanel/screens/ProjectFundingSelectionFormScreen.tsx index 07fff256a..01ca74b1a 100644 --- a/src/pages/projectView/projectActivityPanel/screens/ProjectFundingSelectionFormScreen.tsx +++ b/src/pages/projectView/projectActivityPanel/screens/ProjectFundingSelectionFormScreen.tsx @@ -19,9 +19,9 @@ import { MAX_FUNDING_AMOUNT_USD } from '../../../../constants' import { useFundCalc } from '../../../../helpers/fundingCalculation' import { IFundForm } from '../../../../hooks' import { IProjectType } from '../../../../interfaces' -import { ProjectReward } from '../../../../types/generated/graphql' +import { ProjectRewardForCreateUpdateFragment } from '../../../../types/generated/graphql' import { useNotification } from '../../../../utils' -import { ProjectPaymentFormFundingComment } from '../../projectMainBody/components/ProjectPaymentFormFundingComment' +import { ProjectFundingFormCommentField } from '../../projectMainBody/components/ProjectFundingFormCommentField' import { FundingFormSection } from '../components/FundingFormSection' type Props = { @@ -34,7 +34,7 @@ type Props = { setFormState: any handleFund: () => void type: IProjectType - rewards?: ProjectReward[] + rewards?: ProjectRewardForCreateUpdateFragment[] name: string } @@ -59,6 +59,7 @@ export const ProjectFundingSelectionFormScreen = ({ const hasSelectedRewards = formState.rewardsByIDAndCount && Object.entries(formState.rewardsByIDAndCount).length > 0 + const submit = () => { const valid = validateFundingAmount() if (valid) { @@ -125,12 +126,10 @@ export const ProjectFundingSelectionFormScreen = ({ Comment - {formState.rewardsCost && ( diff --git a/src/pages/projectView/projectMainBody/components/FundingFormRewardItem.tsx b/src/pages/projectView/projectMainBody/components/FundingFormRewardItem.tsx index 231510b9b..8bdc9b4af 100644 --- a/src/pages/projectView/projectMainBody/components/FundingFormRewardItem.tsx +++ b/src/pages/projectView/projectMainBody/components/FundingFormRewardItem.tsx @@ -15,7 +15,7 @@ import { ItemCard } from '../../../../components/layouts/ItemCard' import { ImageWithReload } from '../../../../components/ui' import { IRewardCount } from '../../../../interfaces' import { colors } from '../../../../styles' -import { ProjectReward } from '../../../../types/generated/graphql' +import { ProjectRewardForCreateUpdateFragment } from '../../../../types/generated/graphql' import { toInt } from '../../../../utils' const useStyles = createUseStyles({ @@ -45,7 +45,7 @@ const useStyles = createUseStyles({ }) interface IRewardItemProps { - item: ProjectReward + item: ProjectRewardForCreateUpdateFragment updateCount?: (_: IRewardCount) => void count?: number readOnly?: boolean diff --git a/src/pages/projectView/projectMainBody/components/ProjectFundingFormCommentField.tsx b/src/pages/projectView/projectMainBody/components/ProjectFundingFormCommentField.tsx new file mode 100644 index 000000000..1057b751f --- /dev/null +++ b/src/pages/projectView/projectMainBody/components/ProjectFundingFormCommentField.tsx @@ -0,0 +1,207 @@ +import { CloseIcon, SearchIcon } from '@chakra-ui/icons' +import { + Box, + Button, + HStack, + HTMLChakraProps, + Image, + Input, + InputGroup, + InputLeftElement, + InputRightElement, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalOverlay, + Text, + Tooltip, + useBoolean, + useDisclosure, +} from '@chakra-ui/react' +import { GiphyFetch } from '@giphy/js-fetch-api' +import { IGif } from '@giphy/js-types' +import { Grid } from '@giphy/react-components' +import { useCallback, useRef, useState } from 'react' + +import { GifIcon } from '../../../../components/icons' +import { TextArea } from '../../../../components/ui' +import { VITE_APP_GIPHY_API_KEY } from '../../../../constants' +import { useAuthContext } from '../../../../context' +import { useMobileMode } from '../../../../utils' +import { AvatarElement } from './AvatarElement' + +type Props = HTMLChakraProps<'div'> & { + comment: string + setTarget: (_: any) => void + setFormState: any +} + +const giphy = new GiphyFetch(VITE_APP_GIPHY_API_KEY) + +export const ProjectFundingFormCommentField = ({ + comment, + setTarget, + setFormState, + ...rest +}: Props) => { + const isMobile = useMobileMode() + + const textAreaRef = useRef(null) + + const { + isOpen: isGIFModalOpen, + onOpen: onGIFModalOpened, + onClose: onGIFModalClosed, + } = useDisclosure() + + const onGIFModalOpenClick = () => { + textAreaRef.current?.blur() + onGIFModalOpened() + } + + const [gifSearch, setGifSearch] = useState('bitcoin') + const [selectedGIF, setSelectedGIF] = useState(null) + + const [isHoveringOverGIFButton, setIsHoveringOverGIFButton] = + useBoolean(false) + const [focus, setFocus] = useState(true) + + const fetchGifs = useCallback( + (offset: number) => + giphy.search(gifSearch, { offset, sort: 'relevant', limit: 9 }), + [gifSearch], + ) + + const { isAnonymous, loginOnOpen, user } = useAuthContext() + + return ( + + + +