Skip to content

Commit

Permalink
feat: integrate nftstorefront
Browse files Browse the repository at this point in the history
  • Loading branch information
LanfordCai committed Jul 29, 2023
1 parent 0f34c05 commit 1668c55
Show file tree
Hide file tree
Showing 12 changed files with 536 additions and 7 deletions.
15 changes: 8 additions & 7 deletions components/common/Siderbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ export default function Sidebar(props) {
let menuItems = [
{ id: "0", label: `Basic`, link: { pathname: "/account/[account]", query: { account: account } } },
{ id: "1", label: `Key`, link: { pathname: "/account/[account]/key", query: { account: account } } },
{ id: "2", label: `Token`, link: { pathname: "/account/[account]/fungible_token", query: { account: account } } },
{ id: "3", label: `Staking`, link: { pathname: "/account/[account]/staking", query: { account: account } } },
{ id: "2", label: `Staking`, link: { pathname: "/account/[account]/staking", query: { account: account } } },
{ id: "3", label: `Token`, link: { pathname: "/account/[account]/fungible_token", query: { account: account } } },
{ id: "4", label: `Collection`, link: { pathname: "/account/[account]/collection", query: { account: account } } },
{ id: "5", label: `Contract`, link: { pathname: "/account/[account]/contract", query: { account: account } } },
{ id: "5", label: `Storefront`, link: { pathname: "/account/[account]/storefront", query: { account: account } } },
{ id: "6", label: `Contract`, link: { pathname: "/account/[account]/contract", query: { account: account } } },
{
id: "6", label: "Storage", subItems: [
{ id: "6-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } },
{ id: "6-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } },
{ id: "6-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } },
id: "7", label: "Storage", subItems: [
{ id: "7-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } },
{ id: "7-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } },
{ id: "7-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } },
]
}
// { id: "5", label: `Analyzer`, link: { pathname: "/account/[account]/analyzer", query: { account: account } } },
Expand Down
35 changes: 35 additions & 0 deletions components/storefront/Listing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Decimal from "decimal.js"
import Image from "next/image"
import { useEffect, useState } from "react"

// const getImageUrl = (listing) => {
// const url = listing?.nft?.metadata?.imageUrl ?? "/token_placeholder.png"
// return url
// }

const getPaymentTokenSymbol = (listing) => {
const symbol = listing?.paymentTokenInfo?.symbol ?? "UNKN"
return symbol.toUpperCase()
}

export default function Listing(props) {
const { listing, typeId } = props
const symbol = getPaymentTokenSymbol(listing)

return (
<div className={`w-36 h-24 bg-white rounded-2xl flex flex-col gap-y-1 pt-2 pb-2 justify-between items-center shrink-0 overflow-hidden shadow-md ring-1 ring-black ring-opacity-5`}>
{/* <div className="flex justify-center w-full rounded-t-2xl aspect-square bg-drizzle-ultralight relative overflow-hidden">
<Image className={"object-contain"} src={imageUrl} fill alt="" priority sizes="5vw" />
</div> */}
<label className="px-3 break-words overflow-hidden text-ellipsis font-flow font-medium text-xs text-gray-400">
{`${typeId}`}
</label>
<label className="px-3 break-words overflow-hidden text-ellipsis font-flow font-semibold text-sm text-black">
{`${new Decimal(listing.details.salePrice).toString()} ${symbol}`}
</label>
<label className="px-3 font-flow font-medium text-xs text-gray-500">
{`#${listing.details.nftID}`}
</label>
</div>
)
}
35 changes: 35 additions & 0 deletions components/storefront/ListingGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Listing from "./Listing"

const getTypeId = (listing) => {
if (!listing) {
return "Unknown"
}

const rawTypeId = listing.details.nftType.typeID
const comps = rawTypeId.split(".")
return comps[2]
}

export default function ListingGroup(props) {
const { listings } = props
const firstListing = listings[0]
const typeId = getTypeId(firstListing)

return (
<div className="flex flex-col w-full gap-y-5">
<label className="text-lg font-bold">
{`${typeId} (${listings.length})`}
</label>
<div className="p-1 grid grid-cols-7 gap-x-2 gap-y-3 min-w-[1076px]">
{
listings.map((listing, index) => {
return (
<Listing key={`listing-${index}`} listing={listing} typeId={typeId} />
)
})
}
</div>

</div>
)
}
1 change: 1 addition & 0 deletions flow/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ config({
"0xFungibleTokenSwitchboard": publicConfig.fungibleTokenSwitchboardAddress,
"0xFlowbox": publicConfig.flowboxAddress,
"0xFlowviewAccountBookmark": publicConfig.accountBookmarkAddress,
"0xNFTStorefrontV2": publicConfig.nftStorefrontV2Address,
})
14 changes: 14 additions & 0 deletions flow/storefront_scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as fcl from "@onflow/fcl"

export const getListings = async (address) => {
const code = await (await fetch("/scripts/storefront/get_listings.cdc")).text()

const result = await fcl.query({
cadence: code,
args: (arg, t) => [
arg(address, t.Address)
]
})

return result
}
94 changes: 94 additions & 0 deletions flow/storefront_transactions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as fcl from "@onflow/fcl"
import { txHandler } from "./transactions"

export const cleanupGhosted = async (
account,
listingIds,
setTransactionInProgress,
setTransactionStatus
) => {
const txFunc = async () => {
return await doCleanupGhosted(account, listingIds)
}

return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
}

const doCleanupGhosted = async (account, listingIds) => {
const code = await (await fetch("/transactions/storefront/cleanup_ghosted.cdc")).text()

const transactionId = fcl.mutate({
cadence: code,
args: (arg, t) => [
arg(account, t.Address),
arg(listingIds, t.Array(t.UInt64))
],
proposer: fcl.currentUser,
payer: fcl.currentUser,
limit: 9999
})

return transactionId
}

export const cleanupPurchased = async (
account,
listingIds,
setTransactionInProgress,
setTransactionStatus
) => {
const txFunc = async () => {
return await doCleanupPurchased(account, listingIds)
}

return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
}

const doCleanupPurchased = async (account, listingIds) => {
const code = await (await fetch("/transactions/storefront/cleanup_purchased.cdc")).text()

const transactionId = fcl.mutate({
cadence: code,
args: (arg, t) => [
arg(account, t.Address),
arg(listingIds, t.Array(t.UInt64))
],
proposer: fcl.currentUser,
payer: fcl.currentUser,
limit: 9999
})

return transactionId
}


export const cleanupExpired = async (
account,
fromIndex, toIndex,
setTransactionInProgress,
setTransactionStatus
) => {
const txFunc = async () => {
return await doCleanupExpired(account, fromIndex, toIndex)
}

return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
}

const doCleanupExpired = async (account, fromIndex, toIndex) => {
const code = await (await fetch("/transactions/storefront/cleanup_expired.cdc")).text()

const transactionId = fcl.mutate({
cadence: code,
args: (arg, t) => [
arg(account, t.Address),
arg(fromIndex, t.UInt64),
arg(toIndex, t.UInt64)
],
proposer: fcl.currentUser,
payer: fcl.currentUser,
limit: 9999
})

return transactionId
}
175 changes: 175 additions & 0 deletions pages/account/[account]/storefront/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { CodeIcon } from "@heroicons/react/outline"
import * as fcl from "@onflow/fcl"
import Image from "next/image"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import useSWR from "swr"
import Layout from "../../../../components/common/Layout"
import Spinner from "../../../../components/common/Spinner"
import { isValidFlowAddress } from "../../../../lib/utils"
import publicConfig from "../../../../publicConfig"
import Custom404 from "../404"
import { getListings } from "../../../../flow/storefront_scripts"
import ListingGroup from "../../../../components/storefront/ListingGroup"
import { useRecoilState } from "recoil"
import {
transactionStatusState,
transactionInProgressState
} from "../../../../lib/atoms"
import { cleanupGhosted, cleanupPurchased, cleanupExpired } from "../../../../flow/storefront_transactions"
import { useSWRConfig } from 'swr'

const listingsFetcher = async (funcName, address) => {
const listings = await getListings(address)
return listings
}

const groupListings = (listings) => {
let grouped = listings.reduce((acc, listing) => {
const typeId = listing.details.nftType.typeID
if (!acc[typeId]) {
acc[typeId] = [];
}

acc[typeId].push(listing)

return acc
}, {})

for (let type in grouped) {
grouped[type].sort((a, b) => b.listingResourceId.localeCompare(a.listingResourceId))
}

let sortedGroups = Object.keys(grouped)
.sort()
.map(key => grouped[key]);

return sortedGroups
}

export default function Storefront(props) {
const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState)
const [, setTransactionStatus] = useRecoilState(transactionStatusState)
const router = useRouter()
const { account } = router.query
const { mutate } = useSWRConfig()

const [listings, setListings] = useState(null)
const [listingGroups, setListingGroups] = useState(null)
const [user, setUser] = useState({ loggedIn: null })

useEffect(() => fcl.currentUser.subscribe(setUser), [])

const { data: itemsData, error: itemsError } = useSWR(
account && isValidFlowAddress(account) ? ["listingsFetcher", account] : null, listingsFetcher
)

useEffect(() => {
if (itemsData) {
setListings(itemsData)
const groupedListings = groupListings(itemsData.validItems)
setListingGroups(groupedListings)
}
}, [itemsData])

if (!account) {
return <div className="h-screen"></div>
}

if (!isValidFlowAddress(account)) {
return <Custom404 title={"Account may not exist"} />
}

const showItems = () => {
if (!listingGroups) {
return (
<div className="flex mt-10 h-[200px] justify-center">
<Spinner />
</div>
)
}

return (
<>
{listingGroups.length > 0 ?
listingGroups.map((listings, index) => {
return (
<ListingGroup key={`listing-groups-${index}`} listings={listings} />
)
}) :
<div className="flex mt-10 h-[70px] text-gray-400 text-base justify-center">
Nothing found
</div>
}
</>
)
}

return (
<div className="container mx-auto max-w-7xl min-w-[380px] px-2">
<Layout>
<div className="flex w-full flex-col gap-y-3 overflow-auto">
<div className="flex w-full flex-col gap-y-3 overflow-auto">

<div className="p-x flex gap-x-5 justify-between w-full min-w-[1076px]">
<div className="p-2 flex flex-col gap-y-2 justify-between w-full">
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">
{`Listings (${listingGroups ? listingGroups.flat().length : 0})`}
</h1>
{
listings && listings.invalidItems.length > 0 ?
<label className="text-sm text-red-400">
{`There are ${listings.invalidItems.length} invalid listings, which are either ghosted, purchased, or expired.`}
</label> : null
}
</div>
<div className="p-x flex gap-x-2 justify-end w-full">
<button
className={`text-black disabled:bg-drizzle-light disabled:text-gray-500 bg-drizzle hover:bg-drizzle-dark px-3 py-2 text-sm h-9 rounded-2xl font-semibold shrink-0`}
disabled={transactionInProgress}
onClick={async () => {
const ghostedIds = listings.invalidItems.filter(item => item.isGhosted && !item.isPurchased).map(item => item.listingResourceId)
await cleanupGhosted(account, ghostedIds, setTransactionInProgress, setTransactionStatus)
mutate(["listingsFetcher", account])
}}
>
Cleanup Ghosted
</button>
<button
className={`text-black disabled:bg-drizzle-light disabled:text-gray-500 bg-drizzle hover:bg-drizzle-dark px-3 py-2 text-sm h-9 rounded-2xl font-semibold shrink-0`}
disabled={transactionInProgress}
onClick={async () => {
const purchasedIds = listings.invalidItems.filter(item => item.isPurchased).map(item => item.listingResourceId)
await cleanupPurchased(account, purchasedIds, setTransactionInProgress, setTransactionStatus)
mutate(["listingsFetcher", account])
}}
>
Cleanup Purchased
</button>
<button
className={`text-black disabled:bg-drizzle-light disabled:text-gray-500 bg-drizzle hover:bg-drizzle-dark px-3 py-2 text-sm h-9 rounded-2xl font-semibold shrink-0`}
disabled={transactionInProgress}
onClick={async () => {
const itemCount = listings.validItems.length + listings.invalidItems.length - 1
await cleanupExpired(account, "0", `${itemCount}`, setTransactionInProgress, setTransactionStatus)
mutate(["listingsFetcher", account])
}}
>
Cleanup Expired
</button>
</div>
</div>

<div className="px-2 py-2 overflow-x-auto h-screen w-full">
<div className="inline-block min-w-full">
<div className="flex flex-col gap-y-4">
{showItems()}
</div>
</div>
</div>
</div>
</div>
</Layout>
</div>
)
}
Loading

0 comments on commit 1668c55

Please sign in to comment.