From 8e829e9d6da64c3953a089a6ddf967054c7f080c Mon Sep 17 00:00:00 2001 From: yukigesho Date: Tue, 5 Nov 2024 11:22:29 -0800 Subject: [PATCH] feat: refactor `CreateClaim` --- src/components/DisplayAddress.tsx | 45 ++++ src/components/bounty/BountyClaims.tsx | 6 +- src/components/bounty/BountyInfo.tsx | 67 +++-- src/components/bounty/BountyMultiplayer.tsx | 72 ++---- src/components/bounty/ClaimList.tsx | 8 +- src/components/bounty/Voting.tsx | 2 +- src/components/global/FormClaim.tsx | 269 ++++++++++++-------- src/components/layout/ContentBounty.tsx | 7 +- src/components/ui/BountyItem.tsx | 10 +- src/components/ui/Button.tsx | 6 +- src/components/ui/CreateBounty.tsx | 10 +- src/components/ui/CreateClaim.tsx | 105 ++------ 12 files changed, 291 insertions(+), 316 deletions(-) create mode 100644 src/components/DisplayAddress.tsx diff --git a/src/components/DisplayAddress.tsx b/src/components/DisplayAddress.tsx new file mode 100644 index 00000000..f5243447 --- /dev/null +++ b/src/components/DisplayAddress.tsx @@ -0,0 +1,45 @@ +import { useQuery } from '@tanstack/react-query'; +import { getDegenOrEnsName } from '@/utils/web3'; +import Link from 'next/link'; +import { Chain } from '@/utils/types'; + +export default function DisplayAddress({ + chain, + address, +}: { + chain: Chain; + address: string; +}) { + const walletDisplayName = useQuery({ + queryKey: ['getWalletDisplayName', address, chain.slug], + queryFn: () => + getWalletDisplayName({ + address: address, + chainName: chain.slug, + }), + }); + + return ( + + {walletDisplayName.data ?? address} + + ); +} + +async function getWalletDisplayName({ + address, + chainName, +}: { + address: string; + chainName: 'arbitrum' | 'base' | 'degen'; +}) { + const nickname = await getDegenOrEnsName({ address, chainName }); + if (nickname) { + return nickname; + } + + return address.slice(0, 6) + '…' + address.slice(-4); +} diff --git a/src/components/bounty/BountyClaims.tsx b/src/components/bounty/BountyClaims.tsx index b259068c..4712d7eb 100644 --- a/src/components/bounty/BountyClaims.tsx +++ b/src/components/bounty/BountyClaims.tsx @@ -101,13 +101,13 @@ export default function BountyClaims({ bountyId }: { bountyId: string }) { /> )} {claims.hasNextPage && ( -
+
)} diff --git a/src/components/bounty/BountyInfo.tsx b/src/components/bounty/BountyInfo.tsx index 7547761c..71db09de 100644 --- a/src/components/bounty/BountyInfo.tsx +++ b/src/components/bounty/BountyInfo.tsx @@ -5,11 +5,11 @@ import { formatEther } from 'viem'; import { useGetChain } from '@/hooks/useGetChain'; import BountyMultiplayer from '@/components/bounty/BountyMultiplayer'; -import CreateClaim from '@/components/ui/CreateClaim'; import { trpc } from '@/trpc/client'; import { useAccount, useSwitchChain, useWriteContract } from 'wagmi'; import abi from '@/constant/abi/abi'; import { useMutation } from '@tanstack/react-query'; +import DisplayAddress from '@/components/DisplayAddress'; export default function BountyInfo({ bountyId }: { bountyId: string }) { const chain = useGetChain(); @@ -58,60 +58,55 @@ export default function BountyInfo({ bountyId }: { bountyId: string }) { return ( <> -
-
+
+

{bounty.data.title}

-

- {bounty.data.description} -

+

{bounty.data.description}

bounty issuer:{' '} - {bounty.data.issuer} +

-
{formatEther(BigInt(bounty.data.amount))} {chain.currency}
-
{bounty.data.inProgress && - account.address !== bounty.data.issuer ? ( - - ) : ( - - )} + account.isConnected && + account.address === bounty.data.issuer && ( + + )}
diff --git a/src/components/bounty/BountyMultiplayer.tsx b/src/components/bounty/BountyMultiplayer.tsx index 09b2be2a..0bb02675 100644 --- a/src/components/bounty/BountyMultiplayer.tsx +++ b/src/components/bounty/BountyMultiplayer.tsx @@ -1,15 +1,14 @@ -import Link from 'next/link'; import { useState } from 'react'; -import { formatEther } from 'viem'; -import { useQuery } from '@tanstack/react-query'; import { ExpandMoreIcon } from '@/components/global/Icons'; import JoinBounty from '@/components/ui/JoinBounty'; import Withdraw from '@/components/ui/Withdraw'; import { trpc } from '@/trpc/client'; import { Chain } from '@/utils/types'; -import { getDegenOrEnsName } from '@/utils/web3'; import { useAccount } from 'wagmi'; +import DisplayAddress from '@/components/DisplayAddress'; +import { formatEther } from 'viem'; +import { cn } from '@/utils'; export default function BountyMultiplayer({ chain, @@ -50,9 +49,10 @@ export default function BountyMultiplayer({ ? `${participants.data.length} contributors` : 'Loading contributors...'} @@ -63,11 +63,16 @@ export default function BountyMultiplayer({
{participants.isSuccess ? ( participants.data.map((participant) => ( - +

+ +   + {`${formatEther(BigInt(participant.amount))} ${ + chain.currency + }`} +

)) ) : (

Loading addresses…

@@ -86,46 +91,3 @@ export default function BountyMultiplayer({ ); } -function Participant({ - chain, - participant, -}: { - chain: Chain; - participant: { - user: { id: string; ens: string | null; degenName: string | null }; - amount: string; - }; -}) { - const walletDisplayName = useQuery({ - queryKey: ['getWalletDisplayName', participant.user.id, chain.slug], - queryFn: () => - getWalletDisplayName({ - address: participant.user.id, - chainName: chain.slug, - }), - }); - - return ( -
- - {walletDisplayName.data ?? participant.user.id} - {' '} - - {formatEther(BigInt(participant.amount))} {chain.currency} -
- ); -} - -async function getWalletDisplayName({ - address, - chainName, -}: { - address: string; - chainName: 'arbitrum' | 'base' | 'degen'; -}) { - const nickname = await getDegenOrEnsName({ address, chainName }); - if (nickname) { - return nickname; - } - - return address.slice(0, 6) + '…' + address.slice(-4); -} diff --git a/src/components/bounty/ClaimList.tsx b/src/components/bounty/ClaimList.tsx index 48b63e90..685084a7 100644 --- a/src/components/bounty/ClaimList.tsx +++ b/src/components/bounty/ClaimList.tsx @@ -30,7 +30,7 @@ export default function ClaimList({
{votingClaim && (
@@ -51,11 +51,7 @@ export default function ClaimList({ {votingClaim && }
-
+

other claims

diff --git a/src/components/bounty/Voting.tsx b/src/components/bounty/Voting.tsx index aa180e02..18076e4d 100644 --- a/src/components/bounty/Voting.tsx +++ b/src/components/bounty/Voting.tsx @@ -29,7 +29,7 @@ export default function Voting({ bountyId }: { bountyId: string }) { useEffect(() => { fetchVotingData(); - }, [bountyId, chain]); + }); const voteMutation = useMutation({ mutationFn: async ({ diff --git a/src/components/global/FormClaim.tsx b/src/components/global/FormClaim.tsx index 67e3559b..ba1a260c 100644 --- a/src/components/global/FormClaim.tsx +++ b/src/components/global/FormClaim.tsx @@ -1,5 +1,5 @@ import imageCompression from 'browser-image-compression'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { toast } from 'react-toastify'; @@ -10,33 +10,75 @@ import abi from '@/constant/abi/abi'; import Image from 'next/image'; import { useMutation } from '@tanstack/react-query'; -const LINK_IPFS = 'https://beige-impossible-dragon-883.mypinata.cloud/ipfs'; +import { + Dialog, + DialogContent, + DialogActions, + Button, + CircularProgress, + Box, +} from '@mui/material'; -export default function FormClaim({ bountyId }: { bountyId: string }) { - const onDrop = useCallback((acceptedFiles: File[]) => { - const file = acceptedFiles[0]; - setFile(file); - const reader = new FileReader(); - reader.onload = (e: ProgressEvent) => { - if (e.target?.result) { - setPreview(e.target.result.toString()); - } - }; - reader.readAsDataURL(file); - }, []); +const LINK_IPFS = 'https://beige-impossible-dragon-883.mypinata.cloud/ipfs'; - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); - const [preview, setPreview] = useState(''); - const account = useAccount(); +export default function FormClaim({ + bountyId, + open, + onClose, +}: { + bountyId: string; + open: boolean; + onClose: () => void; +}) { + const [preview, setPreview] = useState(''); + const [imageURI, setImageURI] = useState(''); const [name, setName] = useState(''); const [description, setDescription] = useState(''); - const [file, setFile] = useState(null); - const [imageURI, setImageURI] = useState(''); const [uploading, setUploading] = useState(false); - const inputFile = useRef(null); + + const account = useAccount(); const writeContract = useWriteContract({}); const chain = useGetChain(); - const switctChain = useSwitchChain(); + const switchChain = useSwitchChain(); + + const onDrop = useCallback((acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + const selectedFile = acceptedFiles[0]; + const reader = new FileReader(); + + reader.onload = (e: ProgressEvent) => { + if (e.target?.result) { + setPreview(e.target.result.toString()); + handleImageUpload(selectedFile); + } + }; + + reader.readAsDataURL(selectedFile); + } + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + maxFiles: 1, + accept: { + 'image/png': ['.png'], + 'image/jpeg': ['.jpg', '.jpeg'], + }, + disabled: !!imageURI, + }); + + const handleImageUpload = async (file: File) => { + setUploading(true); + try { + const compressedFile = await compressImage(file); + const cid = await retryUpload(compressedFile); + setImageURI(`${LINK_IPFS}/${cid}`); + } catch (error) { + toast.error('Error uploading image'); + } finally { + setUploading(false); + } + }; const compressImage = async (image: File): Promise => { const options = { @@ -44,9 +86,7 @@ export default function FormClaim({ bountyId }: { bountyId: string }) { maxWidthOrHeight: 1920, useWebWorker: true, }; - - const compressedFile = await imageCompression(image, options); - return compressedFile; + return await imageCompression(image, options); }; const retryUpload = async (file: File): Promise => { @@ -58,37 +98,17 @@ export default function FormClaim({ bountyId }: { bountyId: string }) { const cid = await uploadFile(file); return cid.IpfsHash; } catch (error) { - if (attempt === MAX_RETRIES) { - throw error; - } + if (attempt === MAX_RETRIES) throw error; await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); } } - throw new Error('All attempts failed'); + throw new Error('All upload attempts failed'); }; - useEffect(() => { - const uploadImage = async () => { - if (file) { - setUploading(true); - try { - const compressedFile = await compressImage(file); - const cid = await retryUpload(compressedFile); - setImageURI(`${LINK_IPFS}/${cid}`); - } catch (error) { - toast.error('Trouble uploading file'); - } - setUploading(false); - } - }; - - uploadImage(); - }, [file]); - const createClaimMutations = useMutation({ mutationFn: async (bountyId: bigint) => { if (chain.id !== account.chainId) { - await switctChain.switchChainAsync({ chainId: chain.id }); + await switchChain.switchChainAsync({ chainId: chain.id }); } const metadata = buildMetadata(imageURI, name, description); const metadataResponse = await uploadMetadata(metadata); @@ -106,71 +126,110 @@ export default function FormClaim({ bountyId }: { bountyId: string }) { useEffect(() => { if (createClaimMutations.isSuccess) { toast.success('Claim created successfully'); + onClose(); } if (createClaimMutations.isError) { toast.error('Failed to create claim'); } - }, [createClaimMutations.isSuccess, createClaimMutations.isError]); + }, [createClaimMutations.isSuccess, createClaimMutations.isError, onClose]); return ( -
-
- - {isDragActive &&

drop files here…

} - {preview && ( - Preview + +
+ + {isDragActive ? ( +

Drop the image here...

+ ) : ( +

+ {imageURI + ? 'Image uploaded' + : 'Drag & drop or click to upload an image'} +

+ )} + {preview && ( + Preview + )} +
+ + title + setName(e.target.value)} + className='border bg-transparent border-[#D1ECFF] py-2 px-2 rounded-md mb-4 w-full' /> - )} -
- - name - setName(e.target.value)} - className='border bg-transparent border-[#D1ECFF] py-2 px-2 rounded-md mb-4' - /> - - description - - - - - -
+ > + {uploading ? : 'Create Claim'} + + + ); } diff --git a/src/components/layout/ContentBounty.tsx b/src/components/layout/ContentBounty.tsx index 64c625c8..43be136b 100644 --- a/src/components/layout/ContentBounty.tsx +++ b/src/components/layout/ContentBounty.tsx @@ -1,11 +1,14 @@ import BountyClaims from '@/components/bounty/BountyClaims'; import BountyInfo from '@/components/bounty/BountyInfo'; +import CreateClaim from '@/components/ui/CreateClaim'; export default function ContentBounty({ bountyId }: { bountyId: string }) { return ( -
+ <> -
+ +
+ ); } diff --git a/src/components/ui/BountyItem.tsx b/src/components/ui/BountyItem.tsx index bee8ee4f..ce3ac533 100644 --- a/src/components/ui/BountyItem.tsx +++ b/src/components/ui/BountyItem.tsx @@ -5,7 +5,6 @@ import { formatEther } from 'viem'; import { useGetChain } from '@/hooks/useGetChain'; import { UsersRoundIcon } from '@/components/global/Icons'; -import Button from '@/components/ui/Button'; interface Bounty { id: string; @@ -22,9 +21,9 @@ export default function BountyItem({ bounty }: { bounty: Bounty }) { return ( <> -
+
-
+

{bounty.title}

@@ -39,11 +38,6 @@ export default function BountyItem({ bounty }: { bounty: Bounty }) {
{bounty.isMultiplayer && }
-
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index f0aa30a3..bc7d13ee 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,10 +1,10 @@ const Button = ({ children }: { children: React.ReactNode }) => { - const buttonClasses = `border border-[#F15E5F] text-[#F15E5F] rounded-md px-5 py-2`; - return (
diff --git a/src/components/ui/CreateBounty.tsx b/src/components/ui/CreateBounty.tsx index c1009a1b..c049d7f3 100644 --- a/src/components/ui/CreateBounty.tsx +++ b/src/components/ui/CreateBounty.tsx @@ -9,18 +9,14 @@ export default function CreateBounty() { const { isConnected } = useAccount(); return ( -
+
{isConnected && !showForm && (
setShowForm(true)} > - create bounty + create bounty
)} diff --git a/src/components/ui/CreateClaim.tsx b/src/components/ui/CreateClaim.tsx index 2e5f8d4e..e03ae00d 100644 --- a/src/components/ui/CreateClaim.tsx +++ b/src/components/ui/CreateClaim.tsx @@ -1,104 +1,29 @@ -import React, { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; - -import FormClaim from '@/components/global/FormClaim'; +import React, { useState } from 'react'; +import { useAccount } from 'wagmi'; import GameButton from '@/components/global/GameButton'; import ButtonCTA from '@/components/ui/ButtonCTA'; -import { useAccount } from 'wagmi'; +import FormClaim from '@/components/global/FormClaim'; export default function CreateClaim({ bountyId }: { bountyId: string }) { - const account = useAccount(); - const [showForm, setShowForm] = useState(false); - const [isVisible, setIsVisible] = useState(true); - const [deviceType, setDeviceType] = useState<'android' | 'ios' | 'laptop'>(); - - let lastScrollY = 0; - - const handleOpenForm = () => { - if (account.isConnected) { - setShowForm(!showForm); - } else { - toast.error('Please connect wallet to continue'); - } - }; - - const controlButton = () => { - if (deviceType === 'android' || deviceType === 'ios') { - if (window.scrollY > lastScrollY) { - setIsVisible(false); - } else { - setIsVisible(true); - } - lastScrollY = window.scrollY; - } - }; - - useEffect(() => { - if (deviceType === 'android' || deviceType === 'ios') { - window.addEventListener('scroll', controlButton); - return () => { - window.removeEventListener('scroll', controlButton); - }; - } - }); - - useEffect(() => { - const userAgent = navigator.userAgent; - - const isAndroid = userAgent.match(/Android/i); - const isIOS = userAgent.match(/iPhone|iPad|iPod/i); - - if (isAndroid) { - setDeviceType('android'); - } else if (isIOS) { - setDeviceType('ios'); - } else { - setDeviceType('laptop'); - } - }, []); + const { isConnected } = useAccount(); return ( -
- {(deviceType === 'laptop' || isVisible) && ( +
+ {isConnected && !showForm && (
setShowForm(true)} > -
- -
- create claim -
- )} - - {showForm && ( -
- - + + create claim
)} + setShowForm(false)} + open={showForm} + />
); }