Skip to content

Commit

Permalink
feat: multi target bulk send (#12)
Browse files Browse the repository at this point in the history
* feat: account bookmark copy button

* feat: new nft bulk transfer
  • Loading branch information
LanfordCai authored Oct 19, 2023
1 parent eb0c9d7 commit 98dedd6
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 38 deletions.
33 changes: 23 additions & 10 deletions components/bookmark/AccountBookmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import {
transactionStatusState,
showNoteEditorState,
accountBookmarkState,
transactionInProgressState
transactionInProgressState,
showBasicNotificationState,
basicNotificationContentState
} from "../../lib/atoms"
import { removeAccountBookmark } from "../../flow/bookmark_transactions";
import { PencilAltIcon } from "@heroicons/react/outline";
import { DocumentDuplicateIcon, PencilAltIcon } from "@heroicons/react/outline";

export default function AccountBookmark(props) {
const [, setShowBasicNotification] = useRecoilState(showBasicNotificationState)
const [, setBasicNotificationContent] = useRecoilState(basicNotificationContentState)
const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState)
const [, setTransactionStatus] = useRecoilState(transactionStatusState)
const [showNoteEditor, setShowNoteEditor] = useRecoilState(showNoteEditorState)
Expand All @@ -36,14 +40,23 @@ export default function AccountBookmark(props) {
}, undefined, { shallow: true })
}}
>{bookmark.address}</div>
<SolidStar className="text-yellow-400 w-6 h-6 cursor-pointer"
onClick={async () => {
if (transactionInProgress) {
return
}
await removeAccountBookmark(bookmark.address, setTransactionInProgress, setTransactionStatus)
mutate(["accountBookmarksFetcher", user.addr])
}}/>
<div className="flex gap-x-1 items-center justify-center">
<DocumentDuplicateIcon className="cursor-pointer text-gray-700 hover:text-drizzle w-5 h-5"
onClick={async () => {
await navigator.clipboard.writeText(bookmark.address)
setShowBasicNotification(true)
setBasicNotificationContent({ type: "information", title: "Copied!", detail: null })
}} />
<SolidStar className="text-yellow-400 w-6 h-6 cursor-pointer"
onClick={async () => {
if (transactionInProgress) {
return
}
await removeAccountBookmark(bookmark.address, setTransactionInProgress, setTransactionStatus)
mutate(["accountBookmarksFetcher", user.addr])
}} />
</div>

</div>
<div className="flex gap-x-2 items-center justify-between">
<div>{bookmark.note}</div>
Expand Down
12 changes: 10 additions & 2 deletions components/collection/NftBulkTransferModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function NftBulkTransferModal(props) {
<div className="mt-3">
<Dialog.Title as="h3" className="text-xl leading-6 font-bold text-gray-900 mb-4">
{showNftBulkTransfer.mode == "NftBulkTransfer" ?
"NFT Bulk Transfer" : "Unknown"}
"NFT Bulk Transfer" : "Set Recipient"}
</Dialog.Title>
<div className='flex flex-col gap-y-4'>
<div>
Expand Down Expand Up @@ -118,8 +118,16 @@ export default function NftBulkTransferModal(props) {
storagePath, publicPath,
setTransactionInProgress, setTransactionStatus
)
router.reload()
} else if (showNftBulkTransfer.mode == "SetRecipient") {
let tokens = Object.assign({}, selectedTokens)
for (const [tokenId, properties] of Object.entries(tokens)) {
if (properties.isSelected && !properties.recipient) {
properties.recipient = recipient
}
}
setSelectedTokens(tokens)
}
router.reload()
}}
>
{"Confirm"}
Expand Down
176 changes: 176 additions & 0 deletions components/collection/NftBulkTransferPreviewModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Fragment, useEffect, useRef, useState } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { useRecoilState } from "recoil"
import { transactionStatusState, transactionInProgressState, showNftBulkTransferPreviewState } from '../../lib/atoms'
import * as fcl from "@onflow/fcl";
import Image from "next/image"
import { getRarityColor, isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils'
import { useRouter } from 'next/router'
import { useSWRConfig } from 'swr'
import { acceptOwnership, redeemAccount } from '../../flow/hc_transactions';
import { bulkTransferNft } from '../../flow/nft_transactions';

export default function NftBulkTransferPreviewModal(props) {
const router = useRouter()
const { mutate } = useSWRConfig()
const { account: account, collection: collectionPath } = router.query
const { selectedTokens, setSelectedTokens, collection } = props
const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState)
const [, setTransactionStatus] = useRecoilState(transactionStatusState)

const [showNftBulkTransferPreview, setShowNftBulkTransferPreview] = useRecoilState(showNftBulkTransferPreviewState)

useEffect(() => fcl.currentUser.subscribe(setUser), [])
const [user, setUser] = useState({ loggedIn: null })

const cancelButtonRef = useRef(null)

const getPreviewList = () => {
const tokenIds = Object.entries(selectedTokens).filter(([tokenId, properties]) => properties.isSelected && properties.recipient).map(([tokenId, selected]) => tokenId)
const previewList = tokenIds.map((tokenId) => {
const token = selectedTokens[tokenId]
return {
tokenId: tokenId,
recipient: token.recipient,
display: token.display
}
}).sort((a, b) => a.recipient.localeCompare(b.recipient))
return previewList
}

const getPreviewView = () => {
const previewList = getPreviewList()
return (
<div className='flex gap-x-3 p-4 overflow-auto'>
{
previewList.map((token, index) => {
const display = token.display
const rarityColor = getRarityColor(display.rarity ? display.rarity.toLowerCase() : null)
return (
<div key={`preview-${index}`} className='flex flex-col justify-between items-center gap-y-3 w-36'>
<div className={`w-36 text-center bg-green-100 text-green-800 rounded-xl font-flow font-medium text-xs`}>
{`#${index + 1}`}
</div>
<div className={
`ring-1 ring-drizzle w-36 h-60 bg-white rounded-2xl flex flex-col gap-y-1 pb-2 justify-between items-center shrink-0 overflow-hidden shadow-md`
}>
<div className="flex justify-center w-full rounded-t-2xl aspect-square bg-drizzle-ultralight relative overflow-hidden">
<Image className={"object-contain"} src={display.imageSrc || "/token_placeholder.png"} fill alt="" priority sizes="5vw" />
{
display.rarity ?
<div className={`absolute top-2 px-2 ${rarityColor} rounded-full font-flow font-medium text-xs`}>
{`${display.rarity}`.toUpperCase()}
</div> : null
}
</div>
<label className="px-3 max-h-12 break-words overflow-hidden text-ellipsis font-flow font-semibold text-xs text-black">
{`${display.name}`}
</label>
<div className="flex flex-col items-center justify-center">
<div className="flex gap-x-1 justify-center items-center">
{
display.transferrable == true ? null :
<div className={`w-4 h-4 text-center bg-indigo-100 text-indigo-800 rounded-full font-flow font-medium text-xs`}>
{"S"}
</div>
}
<label className="font-flow font-medium text-xs text-gray-400">
{`#${display.tokenID}`}
</label>
</div>

</div>
</div>
<div className={`px-1 text-center bg-indigo-100 text-indigo-800 rounded-xl font-flow font-medium text-sm`}>
{token.recipient}
</div>
</div>
)
})
}
</div>
)
}

return (
<Transition.Root show={showNftBulkTransferPreview.show} as={Fragment}>
<Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={() => setShowNftBulkTransferPreview(prev => ({
...prev, show: false
}))}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end sm:items-center justify-center min-h-full p-2 text-center sm:p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="min-w-[320px] relative bg-white rounded-2xl p-4 text-left overflow-hidden shadow-xl transform transition-all sm:max-w-3xl sm:w-full sm:p-6">
<div>
<div className="mt-3">
<Dialog.Title as="h3" className="text-xl leading-6 font-bold text-gray-900 mb-4">
NFT Bulk Transfer
</Dialog.Title>
<div className='flex flex-col gap-y-4'>
{getPreviewView()}
</div>
</div>
</div>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
<button
type="button"
disabled={transactionInProgress}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-drizzle text-base font-medium text-black hover:bg-drizzle-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-drizzle sm:col-start-2 sm:text-sm disabled:bg-drizzle-light disabled:text-gray-500"
onClick={async () => {
setShowNftBulkTransferPreview(prev => ({
...prev, show: false
}))

const publicPath = `/public/${collection.publicPathIdentifier}`
const storagePath = `/storage/${collection.storagePathIdentifier}`
const list = getPreviewList()
const tokenIds = list.map((token) => token.tokenId)
const recipients = list.map((token) => token.recipient)
await bulkTransferNft(recipients, tokenIds,
storagePath, publicPath,
setTransactionInProgress, setTransactionStatus
)
router.reload()
}}
>
{"Confirm"}
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-drizzle sm:mt-0 sm:col-start-1 sm:text-sm"
onClick={() => setShowNftBulkTransferPreview(prev => ({
...prev, show: false
}))}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
6 changes: 3 additions & 3 deletions components/common/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default function Layout({ children }) {
return null
}
if (!bookmark) {
return <StarIcon className="text-gray-700 hover:text-drizzle w-6 h-6"
return <StarIcon className="cursor-pointer text-gray-700 hover:text-drizzle w-6 h-6"
onClick={async () => {
if (transactionInProgress) {
return
Expand All @@ -122,7 +122,7 @@ export default function Layout({ children }) {
/>
}

return <SolidStar className="text-yellow-400 w-6 h-6"
return <SolidStar className="cursor-pointer text-yellow-400 w-6 h-6"
onClick={async () => {
if (transactionInProgress) {
return
Expand All @@ -149,7 +149,7 @@ export default function Layout({ children }) {
<label className="text-lg sm:text-xl text-gray-500">Account</label>
<div className="flex gap-x-2 items-center">
<label className="text-2xl sm:text-3xl font-bold">{`${account}`}</label>
<DocumentDuplicateIcon className="text-gray-700 hover:text-drizzle w-6 h-6"
<DocumentDuplicateIcon className="cursor-pointer text-gray-700 hover:text-drizzle w-6 h-6"
onClick={async () => {
await navigator.clipboard.writeText(account)
setShowBasicNotification(true)
Expand Down
14 changes: 11 additions & 3 deletions components/common/NFTView.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default function NFTView(props) {
const router = useRouter()
const rarityColor = getRarityColor(display.rarity ? display.rarity.toLowerCase() : null)
const maxSelection = 20
console.log(display)
return (
<div className={
classNames(
Expand Down Expand Up @@ -41,10 +40,10 @@ export default function NFTView(props) {
setShowBasicNotification(true)
setBasicNotificationContent({ type: "exclamation", title: `You can select ${maxSelection} NFTs at most`, detail: null })
} else {
tokens[tokenId] = { isSelected: true, selectedAt: new Date().getTime(), display: display }
tokens[tokenId] = { isSelected: true, selectedAt: new Date().getTime(), display: display, recipient: null }
}
} else {
tokens[tokenId] = { isSelected: false, selectedAt: 0, display: display }
tokens[tokenId] = { isSelected: false, selectedAt: 0, display: display, recipient: null }
}
setSelectedTokens(tokens)
}
Expand All @@ -61,6 +60,7 @@ export default function NFTView(props) {
<label className="px-3 max-h-12 break-words overflow-hidden text-ellipsis font-flow font-semibold text-xs text-black">
{`${display.name}`}
</label>
<div className="flex flex-col items-center justify-center">
<div className="flex gap-x-1 justify-center items-center">
{
display.transferrable == true ? null :
Expand All @@ -72,6 +72,14 @@ export default function NFTView(props) {
{`#${display.tokenID}`}
</label>
</div>
{
selectMode == "Select" && selectedTokens[tokenId] && selectedTokens[tokenId].isSelected && selectedTokens[tokenId].recipient ?
<div className={`px-1 h-4 text-center bg-indigo-100 text-indigo-800 rounded-full font-flow font-medium text-xs`}>
{selectedTokens[tokenId].recipient}
</div>
: null
}
</div>
</div>
)
}
12 changes: 4 additions & 8 deletions flow/nft_transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,26 @@ const doTransferNft = async (address, tokenId, collectionStoragePath, collection
}

export const bulkTransferNft = async (
address, tokenIds, collectionStoragePath, collectionPublicPath,
recipients, tokenIds, collectionStoragePath, collectionPublicPath,
setTransactionInProgress,
setTransactionStatus
) => {
if (!isValidFlowAddress(address)) {
return
}

const txFunc = async () => {
return await doBulkTransferNft(address, tokenIds, collectionStoragePath, collectionPublicPath)
return await doBulkTransferNft(recipients, tokenIds, collectionStoragePath, collectionPublicPath)
}

return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
}

const doBulkTransferNft = async (address, tokenIds, collectionStoragePath, collectionPublicPath) => {
const doBulkTransferNft = async (recipients, tokenIds, collectionStoragePath, collectionPublicPath) => {
const rawCode = await (await fetch("/transactions/collection/bulk_transfer_nft.cdc")).text()
const code = rawCode.replace("__NFT_STORAGE_PATH__", collectionStoragePath)
.replace("__NFT_PUBLIC_PATH__", collectionPublicPath)

const transactionId = fcl.mutate({
cadence: code,
args: (arg, t) => [
arg(address, t.Address),
arg(recipients, t.Array(t.Address)),
arg(tokenIds, t.Array(t.UInt64))
],
proposer: fcl.currentUser,
Expand Down
1 change: 0 additions & 1 deletion flow/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export const getNftViews = async (address, storagePathID, tokenIDs) => {
]
})

console.log(displays)
return displays
}

Expand Down
7 changes: 6 additions & 1 deletion lib/atoms.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ export const showRedeemAccountState = atom({

export const showNftBulkTransferState = atom({
key: "showNftBulkTransferState",
default: {show: false, mode: "NftBulkTransfer"}
default: {show: false, mode: "SetTarget"}
})

export const showNftBulkTransferPreviewState = atom({
key: "showNftBulkTransferPreviewState",
default: {show: false, mode: "NftBulkTransferPreview"}
})

export const showTransferOwnershipState = atom({
Expand Down
Loading

1 comment on commit 98dedd6

@vercel
Copy link

@vercel vercel bot commented on 98dedd6 Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.