diff --git a/package.json b/package.json index 7383a1e..484567c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "graphql-request": "^5.1.0", "lodash": "^4.17.21", "lodash.merge": "^4.6.2", + "millify": "^6.1.0", "ms": "^2.1.3", "numeral": "^2.0.6", "polished": "^4.2.2", diff --git a/src/components/DynamicWrapper/index.tsx b/src/components/DynamicWrapper/index.tsx index 2988b6d..fa98876 100644 --- a/src/components/DynamicWrapper/index.tsx +++ b/src/components/DynamicWrapper/index.tsx @@ -7,6 +7,7 @@ const Uploader = lazy(() => import('@/pages/Uploader')) const Manager = lazy(() => import('@/pages/Manage')) const Auction = lazy(() => import('@/pages/Auction')) const Mint = lazy(() => import('@/pages/Mint')) +const MyCollections = lazy(() => import('@/pages/MyCollections')) export default function DynamicWrapper({ identifier }: { identifier: string }) { const component = useMemo(() => { @@ -25,6 +26,8 @@ export default function DynamicWrapper({ identifier }: { identifier: string }) { return case 'mint': return + case 'myCollections': + return default: return <>> } diff --git a/src/components/Errors/ErrorWrapper/index.tsx b/src/components/Errors/ErrorWrapper/index.tsx index 0c63fc4..b0d3623 100644 --- a/src/components/Errors/ErrorWrapper/index.tsx +++ b/src/components/Errors/ErrorWrapper/index.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components/macro' export const ErrorWrapper = styled.div` - width: 50%; + width: 60%; height: 26%; position: absolute; top: 37%; diff --git a/src/constants/pages.ts b/src/constants/pages.ts index 11943fa..6ed498e 100644 --- a/src/constants/pages.ts +++ b/src/constants/pages.ts @@ -1,6 +1,7 @@ import MiyaLogo from 'assets/134321870.png?preset=icon&resize=true' import ExecutableIcon from 'assets/create_new.png?preset=icon&resize=true' import GearIcon from 'assets/executable_gear.png?preset=icon&resize=true' +import FolderOpen from 'assets/folder_open.png?preset=thumbnail&resize=true' import AuctionIcon from 'assets/icon/auction.png?preset=icon&resize=true' import MintIcon from 'assets/launchpad_logo.png?preset=icon&resize=true' import ManageIcon from 'assets/miya_website_logo_2-removebg-preview.png?preset=icon&resize=true' @@ -21,7 +22,7 @@ const Pages: Record = { home: { id: 'home', path: '/about', - label: 'Net Mint', + label: 'Net Explorer', icon: MiyaLogo[0], minSize: { width: 800, @@ -38,6 +39,16 @@ const Pages: Record = { height: 720, }, }, + myCollections: { + id: 'myCollections', + path: '/my-collections', + label: 'My Collections', + icon: FolderOpen[0], + minSize: { + width: 800, + height: 720, + }, + }, launch: { id: 'launch', path: '/launch', diff --git a/src/pages/Mint/Collection/CollectionDetail.tsx b/src/pages/Mint/Collection/CollectionDetail.tsx index fd54f0e..0a8b49a 100644 --- a/src/pages/Mint/Collection/CollectionDetail.tsx +++ b/src/pages/Mint/Collection/CollectionDetail.tsx @@ -25,7 +25,7 @@ import { Spinner } from '@/components/Spinner' import BackButton from '@/pages/Mint/Button/BackButton' import ConnectWalletButton from '@/pages/Mint/Button/ConnectWalletButton' import MintButton from '@/pages/Mint/Button/MintButton' -import { EXPLORER_PAGE_SECTION } from '@/pages/Mint/constants' +import { MINT_PAGE_SECTION } from '@/pages/Mint/constants' import type { Collection } from '@/pages/Mint/types/collection' import { useContractMetadata } from '@/pages/Mint/useContractMetadata' import { useMintWithFee } from '@/pages/Mint/useMintWithFee' @@ -366,7 +366,7 @@ export default function CollectionDetail({ {/* */} - setPageSection(EXPLORER_PAGE_SECTION.COLLECTIONS_SECTION)} /> + setPageSection(MINT_PAGE_SECTION.COLLECTIONS_SECTION)} /> @@ -388,7 +388,7 @@ export default function CollectionDetail({ value={freeMintAmount} onChange={(e) => setFreeMintAmount(Number(e.target.value))} /> - + diff --git a/src/pages/Mint/Collections/CollectionsItem.tsx b/src/pages/Mint/Collections/CollectionsItem.tsx index 9c603f1..d5af9d5 100644 --- a/src/pages/Mint/Collections/CollectionsItem.tsx +++ b/src/pages/Mint/Collections/CollectionsItem.tsx @@ -1,7 +1,6 @@ import styled from 'styled-components/macro' import SampleImage from '@/assets/explorer/sample/nft_2.png' -import { EXPLORER_PAGE_SECTION } from '@/pages/Mint/constants' import type { Collection } from '@/pages/Mint/types/collection' import { useContractMetadata } from '@/pages/Mint/useContractMetadata' diff --git a/src/pages/Mint/Collections/index.tsx b/src/pages/Mint/Collections/index.tsx index ea51f9b..63f2d50 100644 --- a/src/pages/Mint/Collections/index.tsx +++ b/src/pages/Mint/Collections/index.tsx @@ -4,11 +4,11 @@ import styled from 'styled-components/macro' import SearchIcon from '@/assets/explorer/icon/search.svg' import { Spinner } from '@/components/Spinner' -import { GET_COLLECTIONS } from '@/pages/Mint/gql/collections' import BackButton from '@/pages/Mint/Button/BackButton' import ConnectWalletButton from '@/pages/Mint/Button/ConnectWalletButton' import CollectionsItem from '@/pages/Mint/Collections/CollectionsItem' -import { EXPLORER_PAGE_SECTION } from '@/pages/Mint/constants' +import { MINT_PAGE_SECTION } from '@/pages/Mint/constants' +import { GET_COLLECTIONS } from '@/pages/Mint/gql/collections' import type { Collection } from '@/pages/Mint/types/collection' const Wrapper = styled.div` @@ -141,7 +141,7 @@ export default function Collections({ const handleClickCollection = (collection: Collection) => { setSelectedCollection(collection) - setPageSection(EXPLORER_PAGE_SECTION.COLLECTION_SECTION) + setPageSection(MINT_PAGE_SECTION.COLLECTION_SECTION) } return ( diff --git a/src/pages/Mint/NFT/index.tsx b/src/pages/Mint/NFT/index.tsx index 20e0730..1a25800 100644 --- a/src/pages/Mint/NFT/index.tsx +++ b/src/pages/Mint/NFT/index.tsx @@ -5,7 +5,7 @@ import { useWindowSize } from 'usehooks-ts' import Dialog from '@/components/Dialog' import type { Token } from '@/pages/Mint' import BackButton from '@/pages/Mint/Button/BackButton' -import { EXPLORER_PAGE_SECTION } from '@/pages/Mint/constants' +import { MINT_PAGE_SECTION } from '@/pages/Mint/constants' const Container = styled.div` height: 100%; @@ -163,7 +163,7 @@ export default function NFTDetail({ 640 ? '25%' : '15%' }}> - setPageSection(EXPLORER_PAGE_SECTION.COLLECTION_SECTION)} /> + setPageSection(MINT_PAGE_SECTION.COLLECTION_SECTION)} /> ) diff --git a/src/pages/Mint/RecentMint/NFTItem.tsx b/src/pages/Mint/RecentMint/NFTItem.tsx index c37a6d2..2a70b10 100644 --- a/src/pages/Mint/RecentMint/NFTItem.tsx +++ b/src/pages/Mint/RecentMint/NFTItem.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components/macro' import type { Address } from 'viem' import type { Token } from '@/pages/Mint' -import { EXPLORER_PAGE_SECTION } from '@/pages/Mint/constants' +import { MINT_PAGE_SECTION } from '@/pages/Mint/constants' import { useTokenMetadata } from '@/pages/Mint/useTokenMetadata' const Container = styled.div` @@ -68,7 +68,7 @@ export default function NFTItem({ collectionAddress, metadata: { name, image, description, attributes, external_url: externalURL }, }) - setPageSection(EXPLORER_PAGE_SECTION.NFT_SECTION) + setPageSection(MINT_PAGE_SECTION.NFT_SECTION) } return ( diff --git a/src/pages/Mint/RecentMint/index.tsx b/src/pages/Mint/RecentMint/index.tsx index c4d8cc4..7efea8a 100644 --- a/src/pages/Mint/RecentMint/index.tsx +++ b/src/pages/Mint/RecentMint/index.tsx @@ -2,7 +2,6 @@ import { useQuery } from '@apollo/client' import type { CSSProperties } from 'react' import { useEffect, useState } from 'react' import styled from 'styled-components/macro' -import { useAccount } from 'wagmi' import { Spinner } from '@/components/Spinner' import type { Token } from '@/pages/Mint' @@ -62,14 +61,11 @@ export default function RecentMint({ const [currentCounter, setCurrentCounter] = useState(1) const [isEndPage, setIsEndPage] = useState(true) - const { address } = useAccount() - const { loading, error, data, refetch } = useQuery(GET_NFTS_OF_USER, { variables: { offset: (currentCounter - 1) * ITEM_PER_PAGE, limit: ITEM_PER_PAGE, collectionAddress: selectedCollection.address, - ownerAddress: address || '', }, }) diff --git a/src/pages/Mint/constants.ts b/src/pages/Mint/constants.ts index ff2e544..cdb84dc 100644 --- a/src/pages/Mint/constants.ts +++ b/src/pages/Mint/constants.ts @@ -1,4 +1,4 @@ -export const EXPLORER_PAGE_SECTION = { +export const MINT_PAGE_SECTION = { COLLECTIONS_SECTION: 'Collections', COLLECTION_SECTION: 'Collection', NFT_SECTION: 'Nft', diff --git a/src/pages/Mint/gql/NFTs.ts b/src/pages/Mint/gql/NFTs.ts index 8cd31ef..59a0d1f 100644 --- a/src/pages/Mint/gql/NFTs.ts +++ b/src/pages/Mint/gql/NFTs.ts @@ -1,12 +1,8 @@ import { gql } from '@apollo/client' export const GET_NFTS_OF_USER = gql` - query GetNFTsOfUser($limit: Int, $offset: Int, $collectionAddress: String, $ownerAddress: String) { - Token( - offset: $offset - limit: $limit - where: { collection: { address: { _eq: $collectionAddress } }, owner_id: { _eq: $ownerAddress } } - ) { + query GetNFTsOfUser($limit: Int, $offset: Int, $collectionAddress: String) { + Token(offset: $offset, limit: $limit, where: { collection: { address: { _eq: $collectionAddress } } }) { metadataUri token_id owner_id diff --git a/src/pages/Mint/index.tsx b/src/pages/Mint/index.tsx index 45d2c97..b4d8e0e 100644 --- a/src/pages/Mint/index.tsx +++ b/src/pages/Mint/index.tsx @@ -11,7 +11,7 @@ import WindowWrapper from '@/components/WindowWrapper' import Pages from '@/constants/pages' import CollectionInfo from '@/pages/Mint/Collection' import Collections from '@/pages/Mint/Collections' -import { EXPLORER_PAGE_SECTION } from '@/pages/Mint/constants' +import { MINT_PAGE_SECTION } from '@/pages/Mint/constants' import NFTDetail from '@/pages/Mint/NFT' import type { Collection } from '@/pages/Mint/types/collection' import type { TokenMetadata } from '@/store/collections/reducer' @@ -74,7 +74,7 @@ export default function MintPage() { const [errorMessage, setErrorMessage] = useState('') const [errorName, setErrorName] = useState('Mint Error') - const [pageSection, setPageSection] = useState(EXPLORER_PAGE_SECTION.COLLECTIONS_SECTION) + const [pageSection, setPageSection] = useState(MINT_PAGE_SECTION.COLLECTIONS_SECTION) const [selectedCollection, setSelectedCollection] = useState({ name: '', metadataUri: '', @@ -102,7 +102,7 @@ export default function MintPage() { const renderSection = () => { switch (pageSection) { - case EXPLORER_PAGE_SECTION.COLLECTIONS_SECTION: + case MINT_PAGE_SECTION.COLLECTIONS_SECTION: return ( ) - case EXPLORER_PAGE_SECTION.COLLECTION_SECTION: + case MINT_PAGE_SECTION.COLLECTION_SECTION: return ( ) - case EXPLORER_PAGE_SECTION.NFT_SECTION: + case MINT_PAGE_SECTION.NFT_SECTION: return default: return <>> diff --git a/src/pages/Mint/useContractMetadata.ts b/src/pages/Mint/useContractMetadata.ts index 20470ae..3859611 100644 --- a/src/pages/Mint/useContractMetadata.ts +++ b/src/pages/Mint/useContractMetadata.ts @@ -5,10 +5,13 @@ import type { ContractMetadata } from '@/store/collections/reducer' export function useContractMetadata({ metadataUri }: { metadataUri: string }) { let contractUri = metadataUri if (contractUri.includes('ipfs://')) { - const uri = contractUri.replace('ipfs://', '') - const cid = uri.split('/')[0] - const contractFile = uri.split('/')[1] - contractUri = `https://${cid}.ipfs.nftstorage.link/${contractFile}` + const imageCID = contractUri.replace('ipfs://', '') + contractUri = `https://nftstorage.link/ipfs/${imageCID}` + } + + if (contractUri.includes('ar://')) { + const imageCID = contractUri.replace('ar://', '') + contractUri = `https://gateway.irys.xyz/${imageCID}` } const { data: contractMetadata, error } = useFetch(contractUri) @@ -19,10 +22,13 @@ export function useContractMetadata({ metadataUri }: { metadataUri: string }) { let imageUri = contractMetadata?.image || '' if (imageUri.includes('ipfs://')) { - imageUri = (contractMetadata?.image || '').replace('ipfs://', '') - const cid = imageUri.split('/')[0] - const imageFile = imageUri.split('/')[1] - imageUri = `https://${cid}.ipfs.nftstorage.link/${imageFile}` + const imageCID = (contractMetadata?.image || '').replace('ipfs://', '') + imageUri = `https://nftstorage.link/ipfs/${imageCID}` + } + + if (imageUri.includes('ar://')) { + const imageCID = (contractMetadata?.image || '').replace('ar://', '') + imageUri = `https://gateway.irys.xyz/${imageCID}` } return { diff --git a/src/pages/Mint/useTokenMetadata.tsx b/src/pages/Mint/useTokenMetadata.tsx index 62f9771..efbf1bd 100644 --- a/src/pages/Mint/useTokenMetadata.tsx +++ b/src/pages/Mint/useTokenMetadata.tsx @@ -26,7 +26,11 @@ export function useTokenMetadata({ address, tokenId }: { address: Address; token if (uri.includes('ar://')) { const cid = uri.replace('ar://', '') - link = `https://gateway.irys.xyz/${cid}` + if (!cid.includes('.json')) { + link = `https://gateway.irys.xyz/${cid}.json` + } else { + link = `https://gateway.irys.xyz/${cid}` + } } const response = await fetch(link) diff --git a/src/pages/MyCollections/Collection/NFTs/NFTDetail.tsx b/src/pages/MyCollections/Collection/NFTs/NFTDetail.tsx new file mode 100644 index 0000000..0ffd88e --- /dev/null +++ b/src/pages/MyCollections/Collection/NFTs/NFTDetail.tsx @@ -0,0 +1,170 @@ +import NFTImage from 'assets/explorer/sample/milady.png' +import styled from 'styled-components/macro' +import { useWindowSize } from 'usehooks-ts' + +import Dialog from '@/components/Dialog' +import type { Token } from '@/pages/Mint' +import BackButton from '@/pages/Mint/Button/BackButton' +import { MY_COLLECTIONS_PAGE_SECTION } from '@/pages/MyCollections/constants' + +const Container = styled.div` + height: 100%; + width: 100%; + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +` + +const DetailWrapper = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + > * + * { + margin-left: 1rem; + } + + @media only screen and (max-width: 640px) { + flex-direction: column; + > * + * { + margin-left: 0; + margin-top: 1rem; + } + } +` +const ImageWrapper = styled.div` + width: 40%; + height: 100%; + + display: flex; + justify-content: center; + align-items: center; + + @media only screen and (max-width: 640px) { + width: 80%; + height: 40%; + align-items: start; + } +` +const Image = styled.img` + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + object-fit: contain; + border: none; + margin: 0; + border-radius: 5px; + + :hover { + border: none; + } +` + +const MetadataWrapper = styled(Dialog)` + width: 60%; + height: 90%; + + @media only screen and (max-width: 640px) { + width: 100%; + height: 60%; + } +` + +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; +` + +const Traits = styled.div` + display: flex; + flex-wrap: wrap; +` +const TraitWrapper = styled.div` + width: calc(100% / 2); + height: 100%; + padding: 0.5rem; + + overflow-y: auto; + overflow-x: hidden; + + ::-webkit-scrollbar { + display: none; + } + + //@media only screen and (max-width: 640px) { + // width: calc(100% / 2); + //} +` +const Trait = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 0.5rem; + + border-radius: 8px; + border: 1px solid #8d8d8d; + + > * + * { + margin-top: 0.25rem; + } +` +const TraitTitle = styled.div` + text-align: center; + color: #808080; + font-size: 0.85rem; +` +const TraitValue = styled.div` + text-align: center; + color: #fff; + font-size: 0.75rem; + + overflow-x: auto; + + ::-webkit-scrollbar { + display: none; + } +` + +export default function NFTDetail({ + setPageSection, + selectedToken, +}: { + setPageSection: (section: string) => void + selectedToken: Token +}) { + const { width } = useWindowSize() + return ( + + 640 ? '75%' : '85%' }}> + + + + + + {selectedToken.metadata.name} #{selectedToken.tokenId} + + {selectedToken.metadata.description} + + {selectedToken.metadata.attributes.map(({ trait_type, value }, index) => ( + + + {trait_type} + {value} + + + ))} + + + + 640 ? '25%' : '15%' }}> + setPageSection(MY_COLLECTIONS_PAGE_SECTION.COLLECTION_SECTION)} /> + + + ) +} diff --git a/src/pages/MyCollections/Collection/NFTs/NFTItem.tsx b/src/pages/MyCollections/Collection/NFTs/NFTItem.tsx new file mode 100644 index 0000000..f1f5e21 --- /dev/null +++ b/src/pages/MyCollections/Collection/NFTs/NFTItem.tsx @@ -0,0 +1,89 @@ +import styled from 'styled-components/macro' +import type { Address } from 'viem' + +import type { Token } from '@/pages/Mint' +import { useTokenMetadata } from '@/pages/Mint/useTokenMetadata' +import { MY_COLLECTIONS_PAGE_SECTION } from '@/pages/MyCollections/constants' + +const Container = styled.div` + width: 25%; + padding: 0.75rem; + cursor: pointer; + + @media only screen and (max-width: 640px) { + width: 50%; + } +` + +const Wrapper = styled.div` + border-radius: 5%; + background-color: #262626; +` + +const ImageWrapper = styled.div` + height: 80%; + width: 100%; +` +const Image = styled.img` + height: auto; + width: auto; + max-height: 100%; + max-width: 100%; + border: none; + border-top-left-radius: 5%; + border-top-right-radius: 5%; + + :hover { + border: none; + } +` + +const DescriptionWrapper = styled.div` + padding: 0.5rem; +` + +const Description = styled.div` + color: #fff; + font-size: 0.5rem; + font-weight: bolder; +` + +export default function NFTItem({ + tokenId, + collectionAddress, + setPageSection, + setSelectedToken, +}: { + tokenId: string + collectionAddress: string + setPageSection: (section: string) => void + setSelectedToken: (token: Token) => void +}) { + const { name, image, description, attributes, externalURL } = useTokenMetadata({ + address: collectionAddress as Address, + tokenId, + }) + + const handleClick = () => { + setSelectedToken({ + tokenId, + collectionAddress, + metadata: { name, image, description, attributes, external_url: externalURL }, + }) + setPageSection(MY_COLLECTIONS_PAGE_SECTION.NFT_SECTION) + } + + return ( + + + + + + + #{tokenId} + {name} + + + + ) +} diff --git a/src/pages/MyCollections/Collection/NFTs/Pagination.tsx b/src/pages/MyCollections/Collection/NFTs/Pagination.tsx new file mode 100644 index 0000000..bf61f44 --- /dev/null +++ b/src/pages/MyCollections/Collection/NFTs/Pagination.tsx @@ -0,0 +1,117 @@ +import NextIcon from 'assets/explorer/icon/next.svg' +import PreviousIcon from 'assets/explorer/icon/previous.svg' +import { useEffect, useRef } from 'react' +import styled from 'styled-components/macro' + +import { NormalButton } from '@/components/Button/NormalButton' + +const PaginateWrapper = styled.div` + height: 10%; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 1rem; +` + +const ButtonContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding-top: 0.75rem; + + > * + * { + margin-left: 1rem; + } +` + +const Icon = styled.img` + display: flex; + justify-content: center; + align-items: center; + border: none; + width: 1rem; + height: 1rem; + + :hover { + border: none; + } +` + +const Counter = styled.div` + text-align: center; + font-weight: 900; +` + +export default function Pagination({ + isEndPage, + currentPage, + setCurrentCounter, +}: { + isEndPage: boolean + currentPage: number + setCurrentCounter: (counter: number) => void +}) { + const previousButtonRef = useRef(null) + const nextButtonRef = useRef(null) + + const handleClickButton = (type: string) => { + let current = currentPage + if (type === 'next') { + if (current === 1 && previousButtonRef.current) { + previousButtonRef.current.disabled = false + } + + if (!isEndPage) { + current += 1 + setCurrentCounter(current) + } + + return + } + + if (!isEndPage && nextButtonRef.current) { + nextButtonRef.current.disabled = false + } + + if (current > 1) { + current -= 1 + setCurrentCounter(current) + } + } + + useEffect(() => { + if (nextButtonRef.current && isEndPage) { + nextButtonRef.current.disabled = true + } + }, [isEndPage]) + + useEffect(() => { + if (previousButtonRef.current && currentPage === 1) { + previousButtonRef.current.disabled = true + } + }, [currentPage]) + + return ( + + + handleClickButton('previous')} + > + + + {currentPage} + handleClickButton('next')} + > + + + + + ) +} diff --git a/src/pages/MyCollections/Collection/NFTs/index.tsx b/src/pages/MyCollections/Collection/NFTs/index.tsx new file mode 100644 index 0000000..e1567fb --- /dev/null +++ b/src/pages/MyCollections/Collection/NFTs/index.tsx @@ -0,0 +1,112 @@ +import { useQuery } from '@apollo/client' +import type { CSSProperties } from 'react' +import { useEffect, useState } from 'react' +import styled from 'styled-components/macro' +import { useAccount } from 'wagmi' + +import { Spinner } from '@/components/Spinner' +import type { Token } from '@/pages/Mint' +import { GET_NFTS_OF_USER } from '@/pages/Mint/gql/NFTs' +import type { Collection } from '@/pages/Mint/types/collection' +import NFTItem from '@/pages/MyCollections/Collection/NFTs/NFTItem' +import Pagination from '@/pages/MyCollections/Collection/NFTs/Pagination' +import type { CollectionBaseInfo } from '@/pages/MyCollections/types/token' + +const Container = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +` + +const NFTList = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-wrap: wrap; + + overflow-y: auto; + -ms-overflow-style: none; + + ::-webkit-scrollbar { + display: none; + } +` + +const ITEM_PER_PAGE = 8 + +type NFT = { + metadataUri: string + token_id: string + owner_id: string + collection: Collection +} + +export default function NFTs({ + style, + setPageSection, + selectedCollection, + setSelectedToken, +}: { + style?: CSSProperties + setPageSection: (section: string) => void + selectedCollection: CollectionBaseInfo + setSelectedToken: (token: Token) => void +}) { + const [currentCounter, setCurrentCounter] = useState(1) + const [isEndPage, setIsEndPage] = useState(true) + + const { address } = useAccount() + + const { loading, error, data } = useQuery(GET_NFTS_OF_USER, { + variables: { + offset: (currentCounter - 1) * ITEM_PER_PAGE, + limit: ITEM_PER_PAGE, + collectionAddress: selectedCollection.address, + ownerAddress: address || '', + }, + }) + + useEffect(() => { + if (data) { + if ((data.Token as NFT[]).length < ITEM_PER_PAGE) { + setIsEndPage(true) + } else { + setIsEndPage(false) + } + } + }, [data]) + + return ( + + {loading || error ? ( + + + + ) : ( + <> + + {(data.Token as NFT[]).map(({ token_id, collection }, index) => ( + + ))} + + + > + )} + + ) +} diff --git a/src/pages/MyCollections/Collection/index.tsx b/src/pages/MyCollections/Collection/index.tsx new file mode 100644 index 0000000..9f9acac --- /dev/null +++ b/src/pages/MyCollections/Collection/index.tsx @@ -0,0 +1,98 @@ +import BackgroundButton from 'assets/explorer/background/background_button.png' +import styled from 'styled-components/macro' + +import type { Token } from '@/pages/MyCollections' +import NFTs from '@/pages/MyCollections/Collection/NFTs' +import { MY_COLLECTIONS_PAGE_SECTION } from '@/pages/MyCollections/constants' +import type { CollectionBaseInfo } from '@/pages/MyCollections/types/token' + +const CollectionContainer = styled.div` + height: 100%; + width: 100%; + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + + > * + * { + margin-top: 0.5rem; + } +` + +const TitleContainer = styled.div` + height: 10%; + width: 100%; + position: relative; + display: flex; + justify-content: center; + align-items: center; +` + +const BackButtonContainer = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: flex-start; + align-items: center; +` +const BackButton = styled.button` + background-color: #f36391; + background-image: url(${BackgroundButton}); + background-blend-mode: overlay; + background-size: 100% 100%; + + width: 3.5rem; + height: 3.5rem; + + color: white; + text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000, 1px 1px #000, -1px -1px #000, 1px -1px #000, + -1px 1px #000; + text-transform: uppercase; + font-size: 0.65rem; + + border: none; + border-radius: 50%; + box-shadow: 4px 2px #272526; + cursor: pointer; + + :hover { + filter: brightness(120%); + } +` + +const Title = styled.div` + text-shadow: 1px 0 #000, -1px 0 #000, 0 1px #000, 0 -1px #000, 1px 1px #000, -1px -1px #000, 1px -1px #000, + -1px 1px #000; + color: white; + font-weight: bolder; +` + +export default function CollectionInfo({ + setPageSection, + selectedCollection, + setSelectedToken, +}: { + setPageSection: (section: string) => void + selectedCollection: CollectionBaseInfo + setSelectedToken: (value: Token) => void +}) { + return ( + + + + setPageSection(MY_COLLECTIONS_PAGE_SECTION.COLLECTIONS_SECTION)}>Back + + Minted NFTs + + 640 ? '42%' : '35%' }} + setPageSection={setPageSection} + selectedCollection={selectedCollection} + setSelectedToken={setSelectedToken} + /> + + ) +} diff --git a/src/pages/MyCollections/Collections/CollectionsItem.tsx b/src/pages/MyCollections/Collections/CollectionsItem.tsx new file mode 100644 index 0000000..89b2402 --- /dev/null +++ b/src/pages/MyCollections/Collections/CollectionsItem.tsx @@ -0,0 +1,105 @@ +import { useQuery } from '@apollo/client' +import millify from 'millify' +import styled from 'styled-components/macro' + +import SampleImage from '@/assets/explorer/sample/nft_2.png' +import { useContractMetadata } from '@/pages/Mint/useContractMetadata' +import { GET_USER_NFTS_BY_COLLECTION } from '@/pages/MyCollections/gql/collections' +import type { CollectionBaseInfo } from '@/pages/MyCollections/types/token' + +const CollectionListItemWrapper = styled.div` + display: flex; + align-items: center; + cursor: pointer; + + padding: 0.5rem; + border-radius: 0.75rem; + + > * + * { + margin-left: 0.5rem; + } + + :hover { + background-color: rgba(204, 144, 204, 0.7); + } +` + +const CollectionItemNameWrapper = styled.div` + display: flex; + justify-content: space-between; + width: 75%; + font-weight: bolder; + text-shadow: 1px 0 #fff, -1px 0 #fff, 0 1px #fff, 0 -1px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, + -1px 1px #fff; + + > p { + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0; + } +` + +const CollectionApprovedName = styled.p` + font-size: 0.75rem; + color: rgb(59, 48, 107); +` + +const NFTsOwnedSection = styled.div` + font-size: 0.75rem; + display: flex; + align-items: center; + + > * + * { + margin-left: 0.5rem; + } +` + +const CollectionItemIcon = styled.div` + width: 25%; + + > img { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + border: none; + margin: 0; + border-radius: 5%; + } +` + +export default function CollectionsItem({ + collection, + handleClick, + address, +}: { + collection: CollectionBaseInfo + handleClick: () => void + address: `0x${string}` | undefined +}) { + const { image } = useContractMetadata({ metadataUri: collection.metadataUri }) + const { loading, error, data } = useQuery(GET_USER_NFTS_BY_COLLECTION, { + variables: { ownerAddress: address || '', collectionAddress: collection.address }, + }) + + return ( + + + + + + {collection.name} + + + {millify(loading || error ? 0 : data.Token.length)} owned + + / + + {millify(Number(collection.maxSupply))} supplies + + + + + ) +} diff --git a/src/pages/MyCollections/Collections/index.tsx b/src/pages/MyCollections/Collections/index.tsx new file mode 100644 index 0000000..fed6d1e --- /dev/null +++ b/src/pages/MyCollections/Collections/index.tsx @@ -0,0 +1,104 @@ +import { useQuery } from '@apollo/client' +import styled from 'styled-components/macro' +import { useAccount } from 'wagmi' + +// import SearchIcon from '@/assets/explorer/icon/search.svg' +import { Spinner } from '@/components/Spinner' +import CollectionsItem from '@/pages/MyCollections/Collections/CollectionsItem' +import { MY_COLLECTIONS_PAGE_SECTION } from '@/pages/MyCollections/constants' +import { GET_USER_COLLECTIONS } from '@/pages/MyCollections/gql/collections' +import type { CollectionBaseInfo, CollectionUserOwnedToken } from '@/pages/MyCollections/types/token' + +const Wrapper = styled.div` + height: 100%; + width: 100%; + display: flex; + flex-direction: row; +` + +const CollectionListContainer = styled.div` + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 0; + padding: 0; +` + +const CollectionListItemContainer = styled.div` + width: 100%; + height: calc(100% - 1.3rem); + + overflow-y: auto; + overflow-x: hidden; + padding-left: 2rem; + padding-right: 2rem; +` + +// const CollectionSearch = styled.input` +// width: 100%; +// height: 1.3rem; +// margin-bottom: 0.5rem; +// background: url(${SearchIcon}) no-repeat scroll 0.3rem 0.3rem; +// background-size: 5%; +// padding-left: 30px; +// border: none; +// color: white; +// +// :focus { +// outline: none; +// } +// ` + +export default function Collections({ + setPageSection, + setSelectedCollection, +}: { + setPageSection: (section: string) => void + setSelectedCollection: (collection: CollectionBaseInfo) => void +}) { + const { address } = useAccount() + + const { loading, error, data } = useQuery(GET_USER_COLLECTIONS, { + variables: { offset: 0, limit: 100, ownerAddress: address || '' }, + }) + + const handleClickCollection = (collection: CollectionBaseInfo) => { + setPageSection(MY_COLLECTIONS_PAGE_SECTION.COLLECTION_SECTION) + setSelectedCollection(collection) + } + + return ( + + + {/* */} + + {loading || error ? ( + + + + ) : ( + (data.Token as CollectionUserOwnedToken[]).map((collectionUserOwnedToken, index) => ( + handleClickCollection(collectionUserOwnedToken.collection)} + address={address} + /> + )) + )} + + + + ) +} diff --git a/src/pages/MyCollections/constants.ts b/src/pages/MyCollections/constants.ts new file mode 100644 index 0000000..cce03f0 --- /dev/null +++ b/src/pages/MyCollections/constants.ts @@ -0,0 +1,5 @@ +export const MY_COLLECTIONS_PAGE_SECTION = { + COLLECTIONS_SECTION: 'Collections', + COLLECTION_SECTION: 'Collection', + NFT_SECTION: 'Nft', +} diff --git a/src/pages/MyCollections/gql/collections.ts b/src/pages/MyCollections/gql/collections.ts new file mode 100644 index 0000000..8cec16a --- /dev/null +++ b/src/pages/MyCollections/gql/collections.ts @@ -0,0 +1,25 @@ +import { gql } from '@apollo/client' + +const GET_USER_COLLECTIONS = gql` + query GetUserOwnedNFTCollections($limit: Int, $offset: Int, $ownerAddress: String) { + Token(distinct_on: collection_id, offset: $offset, limit: $limit, where: { owner_id: { _eq: $ownerAddress } }) { + collection { + name + symbol + address + metadataUri + maxSupply + } + } + } +` + +const GET_USER_NFTS_BY_COLLECTION = gql` + query GetUserNFTsByCollection($ownerAddress: String, $collectionAddress: String) { + Token(where: { owner_id: { _eq: $ownerAddress }, collection: { address: { _eq: $collectionAddress } } }) { + metadataUri + token_id + } + } +` +export { GET_USER_COLLECTIONS, GET_USER_NFTS_BY_COLLECTION } diff --git a/src/pages/MyCollections/index.tsx b/src/pages/MyCollections/index.tsx new file mode 100644 index 0000000..2b598e3 --- /dev/null +++ b/src/pages/MyCollections/index.tsx @@ -0,0 +1,130 @@ +import BackgroundImage from 'assets/explorer/background/background.png' +import { useState } from 'react' +import styled from 'styled-components/macro' + +import TitleBar from '@/components/TitleBar' +import WindowWrapper from '@/components/WindowWrapper' +import Pages from '@/constants/pages' +import { MINT_PAGE_SECTION } from '@/pages/Mint/constants' +import CollectionInfo from '@/pages/MyCollections/Collection' +import NFTDetail from '@/pages/MyCollections/Collection/NFTs/NFTDetail' +import Collections from '@/pages/MyCollections/Collections' +import { MY_COLLECTIONS_PAGE_SECTION } from '@/pages/MyCollections/constants' +import type { CollectionBaseInfo } from '@/pages/MyCollections/types/token' +import type { TokenMetadata } from '@/store/collections/reducer' +import { useAppDispatch } from '@/store/hooks' +import { closeWindow, minimizeWindow } from '@/store/windows/actions' +import type { PageKey } from '@/store/windows/reducer' + +const page = Pages.myCollections +const pageId = page?.id as PageKey + +const Background = styled.div` + height: calc(100% - 1.5rem); + width: 100%; + + position: relative; + overflow: hidden; + + :before { + content: ' '; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: 0.6; + background-image: url(${BackgroundImage}); + background-repeat: no-repeat; + background-position: 50% 0; + background-size: cover; + } + + * { + font-family: 'Revalia', sans-serif; + letter-spacing: 1px; + } +` +const Container = styled.div` + position: relative; + display: flex; + flex-direction: column; + padding: 0.5rem; + gap: 1rem; + height: 100%; +` + +export type Token = { + metadata: TokenMetadata + tokenId: string + collectionAddress: string +} + +export default function MyCollectionsPage() { + // Window mgmt + const dispatch = useAppDispatch() + const close = () => dispatch(closeWindow({ value: pageId })) + const minimize = () => dispatch(minimizeWindow({ value: pageId })) + + const [pageSection, setPageSection] = useState(MY_COLLECTIONS_PAGE_SECTION.COLLECTIONS_SECTION) + const [selectedCollection, setSelectedCollection] = useState({ + name: '', + symbol: '', + metadataUri: '', + address: '', + maxSupply: '0', + }) + const [selectedToken, setSelectedToken] = useState({ + metadata: { + name: '', + description: '', + image: '', + external_url: '', + attributes: [], + }, + tokenId: '', + collectionAddress: '', + }) + + const renderSection = () => { + switch (pageSection) { + case MINT_PAGE_SECTION.COLLECTIONS_SECTION: + return + case MINT_PAGE_SECTION.COLLECTION_SECTION: + return ( + + ) + case MINT_PAGE_SECTION.NFT_SECTION: + return + default: + return <>> + } + } + + return ( + + { + if (e.cancelable) e.stopPropagation() + close() + }} + minimizeBtn + onMinimize={(e) => { + if (e.cancelable) e.stopPropagation() + minimize() + }} + > + {page?.label} + + + {renderSection()} + + + ) +} diff --git a/src/pages/MyCollections/types/token.ts b/src/pages/MyCollections/types/token.ts new file mode 100644 index 0000000..e38d218 --- /dev/null +++ b/src/pages/MyCollections/types/token.ts @@ -0,0 +1,12 @@ +type CollectionBaseInfo = { + name: string + symbol: string + address: string + metadataUri: string + maxSupply: string +} +type CollectionUserOwnedToken = { + collection: CollectionBaseInfo +} + +export type { CollectionBaseInfo, CollectionUserOwnedToken } diff --git a/src/pages/OperatingSystem/index.tsx b/src/pages/OperatingSystem/index.tsx index fb895b9..6684a78 100644 --- a/src/pages/OperatingSystem/index.tsx +++ b/src/pages/OperatingSystem/index.tsx @@ -80,6 +80,7 @@ const UploaderPage = Pages.uploader const ManagePage = Pages.manager const AuctionPage = Pages.auction const MintPage = Pages.mint +const MyCollectionPage = Pages.myCollections export default function OperatingSystem() { const location = useLocation() @@ -102,6 +103,7 @@ export default function OperatingSystem() { } const page = Pages[id] + console.log(Pages, id, page) if (!page) return dispatch( openWindow({ @@ -212,8 +214,12 @@ export default function OperatingSystem() { {ComponentPage?.label} */} - null}> - My collections + handleOpen(MyCollectionPage?.id as PageKey)} + > + {MyCollectionPage?.label}