diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 86c8870e..6e1cdde0 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,3 +16,15 @@ jobs:
run: |
yarn install
yarn test
+ test-hooks:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2
+ with:
+ node-version: '15'
+ - name: 'Test hooks'
+ working-directory: ./packages/hooks
+ run: |
+ yarn install
+ yarn test
diff --git a/examples/frontend/package.json b/examples/frontend/package.json
index 83bb225e..98ef96d9 100644
--- a/examples/frontend/package.json
+++ b/examples/frontend/package.json
@@ -8,8 +8,8 @@
"start": "next start"
},
"dependencies": {
- "@cura/components": "0.0.12",
- "@cura/hooks": "0.0.4",
+ "@cura/components": "0.0.14",
+ "@cura/hooks": "0.0.6",
"@runwayml/hosted-models": "^0.3.0",
"@theme-ui/color": "^0.8.4",
"@theme-ui/match-media": "^0.6.0",
diff --git a/examples/frontend/pages/cc/[project]/[id].tsx b/examples/frontend/pages/cc/[project]/[id].tsx
index cff162cb..d4df3ce3 100644
--- a/examples/frontend/pages/cc/[project]/[id].tsx
+++ b/examples/frontend/pages/cc/[project]/[id].tsx
@@ -5,7 +5,7 @@ import { Button, Text } from 'theme-ui'
import { utils } from 'near-api-js'
import { useRouter } from 'next/router'
import Layout from '../../../containers/Layout'
-import { Bidders, RenderIframe, CreatorShare } from '@cura/components'
+import { Bidders, CreatorShare, MediaObject } from '@cura/components'
import { alertMessageState, indexLoaderState } from '../../../state/recoil'
import { useSetRecoilState } from 'recoil'
import { mapPathToProject } from 'utils/path-to-project'
@@ -13,7 +13,6 @@ import { getFrameWidth } from 'utils/frame-width'
import { useNFTContract, useNFTMethod, useMarketMethod } from '@cura/hooks'
import { useStatusUpdate } from 'utils/hooks-helpers'
-const CONTRACT_VIEW_GAS = utils.format.parseNearAmount('0.00000000010') // 100 Tgas
const CONTRACT_BURN_GAS = utils.format.parseNearAmount('0.00000000029') // 290 Tgas
const MARKET_ACCEPT_BID_GAS = utils.format.parseNearAmount('0.00000000025') // 250 Tgas
const YOCTO_NEAR = utils.format.parseNearAmount('0.000000000000000000000001')
@@ -39,7 +38,7 @@ const CCProjectID = ({}) => {
token_id: router.query.id,
limit: 2,
},
- CONTRACT_VIEW_GAS,
+ undefined,
updateStatus
)
@@ -107,9 +106,10 @@ const CCProjectID = ({}) => {
}}
>
{media && (
-
)}
diff --git a/examples/frontend/pages/cc/[project]/create.tsx b/examples/frontend/pages/cc/[project]/create.tsx
index db5d8cb8..88c1cf87 100644
--- a/examples/frontend/pages/cc/[project]/create.tsx
+++ b/examples/frontend/pages/cc/[project]/create.tsx
@@ -6,7 +6,7 @@ import { utils } from 'near-api-js'
import { useRouter } from 'next/router'
import Layout from '../../../containers/Layout'
-import { CreatorShare, RenderIframe } from '@cura/components'
+import { CreatorShare } from '@cura/components'
import { alertMessageState, indexLoaderState } from '../../../state/recoil'
import { useSetRecoilState } from 'recoil'
import { useNFTContract } from '@cura/hooks'
@@ -136,10 +136,13 @@ const MLProjectCreate = ({}) => {
}}
>
{creativeCode && (
-
+ height={frameDimension}
+ frameBorder="0"
+ scrolling="no"
+ >
)}
{
}}
>
{randomDesign?.metadata?.media && (
-
)}
@@ -141,10 +142,7 @@ const Explore = ({}) => {
alignItems: 'center',
}}
>
-
+
{
}}
>
{metadata?.media && (
-
)}
diff --git a/examples/frontend/pages/ml/[project]/[id].tsx b/examples/frontend/pages/ml/[project]/[id].tsx
index dae9208f..dcc2997b 100644
--- a/examples/frontend/pages/ml/[project]/[id].tsx
+++ b/examples/frontend/pages/ml/[project]/[id].tsx
@@ -6,15 +6,13 @@ import { Button, Flex, Box, Text } from 'theme-ui'
import { utils } from 'near-api-js'
import { useRouter } from 'next/router'
import Layout from '../../../containers/Layout'
-import { CreatorShare } from '@cura/components'
-import { Bidders } from '@cura/components'
+import { CreatorShare, Bidders, MediaObject } from '@cura/components'
import { alertMessageState, indexLoaderState } from '../../../state/recoil'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { useNFTContract, useNFTMethod, useMarketMethod } from '@cura/hooks'
import { accountState } from 'state/account'
import { getFrameWidth } from 'utils/frame-width'
import { useStatusUpdate } from 'utils/hooks-helpers'
-const CONTRACT_VIEW_GAS = utils.format.parseNearAmount('0.00000000010') // 100 Tgas
const CONTRACT_BURN_GAS = utils.format.parseNearAmount('0.00000000029') // 290 Tgas
const MARKET_ACCEPT_BID_GAS = utils.format.parseNearAmount('0.00000000025') // 250 Tgas
const YOCTO_NEAR = utils.format.parseNearAmount('0.000000000000000000000001')
@@ -42,7 +40,7 @@ const MLProject = ({}) => {
token_id: router.query.id,
limit: 2,
},
- CONTRACT_VIEW_GAS,
+ null,
updateStatus
)
@@ -110,9 +108,10 @@ const MLProject = ({}) => {
}}
>
{media && (
-
)}
diff --git a/examples/frontend/pages/ml/[project]/create.tsx b/examples/frontend/pages/ml/[project]/create.tsx
index 1bc3bbc5..0720ed75 100644
--- a/examples/frontend/pages/ml/[project]/create.tsx
+++ b/examples/frontend/pages/ml/[project]/create.tsx
@@ -6,7 +6,7 @@ import { utils } from 'near-api-js'
import { useRouter } from 'next/router'
import Layout from '../../../containers/Layout'
import { HostedModel } from '@runwayml/hosted-models'
-import { CreatorShare } from '@cura/components'
+import { CreatorShare, MediaObject } from '@cura/components'
import { alertMessageState, indexLoaderState } from '../../../state/recoil'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { useNFTContract } from '@cura/hooks'
@@ -170,7 +170,7 @@ const MLProjectCreate = ({}) => {
width: frameDimension,
}}
>
-
+
{
}}
>
{randomDesign?.metadata.media && (
-

)}
@@ -138,10 +144,7 @@ const Explore = ({}) => {
alignItems: 'center',
}}
>
-
+
{
},
}}
>
-
diff --git a/examples/frontend/pages/share/create.tsx b/examples/frontend/pages/share/create.tsx
index 6a807921..7eeb520e 100644
--- a/examples/frontend/pages/share/create.tsx
+++ b/examples/frontend/pages/share/create.tsx
@@ -5,7 +5,7 @@ import { useState } from 'react'
import { Button, Text } from 'theme-ui'
import { utils } from 'near-api-js'
import Layout from '../../containers/Layout'
-import { CreatorShare, RenderIframe } from '@cura/components'
+import { CreatorShare } from '@cura/components'
import { alertMessageState, indexLoaderState } from '../../state/recoil'
import { useSetRecoilState } from 'recoil'
import { useNFTContract } from '@cura/hooks'
@@ -128,10 +128,13 @@ const Create = ({}) => {
}}
>
{creativeCode && (
-
+ height={frameDimension}
+ frameBorder="0"
+ scrolling="no"
+ >
)}
{
}}
>
{randomDesign.metadata.media && (
-
)}
@@ -134,10 +135,7 @@ const Explore = ({}) => {
alignItems: 'center',
}}
>
-
+
{
}}
>
{media?.[0]?.metadata?.media && (
-
)}
diff --git a/packages/components/package.json b/packages/components/package.json
index dad8018a..5c714350 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@cura/components",
- "version": "0.0.12",
+ "version": "0.0.14",
"description": "Components for front-ends on NEAR blockchain",
"source": "src/index.ts",
"main": "dist/index.js",
@@ -18,6 +18,8 @@
"author": "Yassine",
"license": "MIT",
"dependencies": {
+ "@cura/hooks": "0.0.6",
+ "@theme-ui/match-media": "^0.12.0",
"@theme-ui/presets": "^0.6.0",
"near-api-js": "^0.42.0",
"react": "^17.0.1",
diff --git a/packages/components/src/MediaObject.tsx b/packages/components/src/MediaObject.tsx
new file mode 100644
index 00000000..c38e1573
--- /dev/null
+++ b/packages/components/src/MediaObject.tsx
@@ -0,0 +1,106 @@
+// @ts-nocheck
+/** @jsxImportSource theme-ui */
+
+import { useNFTContentType } from '@cura/hooks'
+import { Placeholder } from './Placeholder'
+
+type mediaObjectProps = {
+ mediaURI: string
+ width?: number | string
+ height?: number | string
+ loading?: boolean
+ autoPlay?: boolean
+}
+
+function Text({ width, height, content }) {
+ return (
+
+ {content}
+
+ )
+}
+
+function Video({ mediaURI, width, height, autoPlay }: mediaObjectProps) {
+ return (
+
+ )
+}
+
+function Audio({ mediaURI }: mediaObjectProps) {
+ return
+}
+
+function Image({ mediaURI, width, height }: mediaObjectProps) {
+ return (
+
+ )
+}
+
+function Iframe({ mediaURI, width, height }: mediaObjectProps) {
+ return (
+
+ )
+}
+
+export function MediaObject(props: mediaObjectProps) {
+ const { loading, data, content } = useNFTContentType(props.mediaURI)
+
+ if (props.loading || loading) {
+ return (
+
+ )
+ }
+ switch (data) {
+ case 'image':
+ return
+
+ case 'video':
+ return
+
+ case 'audio':
+ return
+
+ case 'text':
+ return
+
+ case 'html' || 'other':
+ return
+
+ default:
+ return <>>
+ }
+}
diff --git a/packages/components/src/Metadata.tsx b/packages/components/src/Metadata.tsx
index a9ba5bce..cfcd2781 100644
--- a/packages/components/src/Metadata.tsx
+++ b/packages/components/src/Metadata.tsx
@@ -1,55 +1,173 @@
-// @ts-nocheck
/** @jsxImportSource theme-ui */
-import { Box, Heading, Text } from 'theme-ui'
+import { Box, Flex } from 'theme-ui'
+import { Placeholder } from './Placeholder'
+
+type NFTMetadataType = {
+ creator_id?: string
+ owner_id?:
+ | string
+ | {
+ Account: string
+ }
+ metadata: {
+ title?: string
+ description?: string
+ }
+}
export function Metadata({
- title,
- creator,
+ data,
+ loading = true,
+ width = 400,
}: {
- title?: string
- creator?: string
+ data: NFTMetadataType
+ loading: boolean
+ width: number | string
}) {
return (
-
- {title}
-
-
- TITLE
-
-
- {creator}
-
-
- CREATOR
-
+ {loading ? : }
)
}
+function MetadataLoaded({
+ metadata: { title, description },
+ creator_id: creator,
+ owner_id: owner,
+}: NFTMetadataType) {
+ return (
+ <>
+ {title && (
+
+ {title}
+
+ )}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {creator && (
+
+
+ Created by
+
+
+ {creator}
+
+
+ )}
+
+ {owner && (
+
+
+ Owned by
+
+
+ {owner}
+
+
+ )}
+
+ >
+ )
+}
+function MetadataLoading() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Created by
+
+
+
+
+
+
+ Owned by
+
+
+
+
+ >
+ )
+}
diff --git a/packages/components/src/NFTE.tsx b/packages/components/src/NFTE.tsx
new file mode 100644
index 00000000..32582bdc
--- /dev/null
+++ b/packages/components/src/NFTE.tsx
@@ -0,0 +1,116 @@
+// @ts-nocheck
+/** @jsxImportSource theme-ui */
+import { useNFT, useNFTContractMetadata, useNFTReference } from '@cura/hooks'
+
+import { Container } from 'theme-ui'
+import { useBreakpointIndex } from '@theme-ui/match-media'
+import { Metadata } from './Metadata'
+import { MediaObject } from './MediaObject'
+
+export function NFTE({
+ contract,
+ tokenId,
+ isDark = false,
+}: {
+ contract: string
+ tokenId: string
+ isDark: boolean
+}) {
+ const canvasSizes = [290, 300, 400, 400, 400]
+ const breakpointIndex = useBreakpointIndex()
+ const canvasSize = canvasSizes[breakpointIndex]
+
+ // Get NFT contract metadata
+ const {
+ error: NFTContractMetadataError,
+ loading: NFTContractMetadataLoading,
+ data: NFTContractMetadata,
+ } = useNFTContractMetadata(contract)
+
+ const baseURI = NFTContractMetadata?.base_uri || 'https://arweave.net/'
+
+ // Get NFT token metadata
+ const {
+ error: NFTError,
+ loading: NFTLoading,
+ data: NFTMetadata,
+ } = useNFT(contract, tokenId)
+
+ const referenceURI = validateURI(NFTMetadata?.metadata?.reference, baseURI)
+
+ // Get NFT reference
+ const {
+ error: NFTReferenceError,
+ loading: NFTReferenceLoading,
+ data: NFTReference,
+ } = useNFTReference(referenceURI)
+
+ // replace null values from NFTMetadata with values from the NFTReference
+ const finalNFTMetadata = {
+ ...NFTMetadata,
+ metadata: {
+ title: NFTMetadata?.metadata?.title || NFTReference?.title,
+ description:
+ NFTMetadata?.metadata?.description || NFTReference?.description,
+ media: NFTMetadata?.metadata?.media || NFTReference?.media,
+ },
+ owner_id: NFTMetadata?.owner_id?.Account || NFTMetadata?.owner_id,
+ creator_id:
+ NFTMetadata?.creator_id?.Account ||
+ NFTMetadata?.creator_id ||
+ NFTMetadata?.creator ||
+ NFTReference?.creator_id,
+ }
+
+ const mediaUri = validateURI(finalNFTMetadata?.metadata?.media, baseURI)
+
+ let error =
+ NFTContractMetadataError ||
+ NFTError ||
+ (!NFTMetadata && { type: 'invalid NFTMetadata' })
+
+ let loading =
+ NFTContractMetadataLoading || NFTLoading || NFTReferenceLoading
+ loading = loading && error
+
+ error && !loading && console.error(error)
+
+ return (
+
+ {error && !loading ? (
+ ❌ Error: {error.type}
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ )
+}
+
+function validateURI(uri = '', base_uri = '') {
+ if (!uri) return
+ if (uri?.includes('http')) return uri
+
+ return base_uri.replace(/\/$/, '') + '/' + uri.replace(/^\//, '')
+}
diff --git a/packages/components/src/Placeholder.tsx b/packages/components/src/Placeholder.tsx
new file mode 100644
index 00000000..c94f38da
--- /dev/null
+++ b/packages/components/src/Placeholder.tsx
@@ -0,0 +1,36 @@
+/** @jsxImportSource theme-ui */
+
+import { keyframes } from '@emotion/react'
+
+const animateph = keyframes`
+from {
+ opacity: .7;
+}
+to {
+ opacity: 1;
+}
+`
+
+export function Placeholder({
+ height = '20px',
+ width = '160px',
+ style,
+}: {
+ height?: number | string
+ width?: number | string
+ style?: object
+}) {
+ return (
+
+ )
+}
diff --git a/packages/components/src/RenderIframe.tsx b/packages/components/src/RenderIframe.tsx
deleted file mode 100644
index 3045b134..00000000
--- a/packages/components/src/RenderIframe.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-nocheck
-/** @jsxImportSource theme-ui */
-
-export function RenderIframe({
- mediaURI,
- code,
- width,
-}: {
- mediaURI?: string
- code?: string
- width?: number
-}) {
- if (code) {
- return (
-
- )
- }
-
- if (mediaURI) {
- return (
-
- )
- }
-
- return null
-}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 46b696c2..83321999 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -2,22 +2,24 @@ import { Bidders } from './Bidders'
import { BidCreate } from './BidCreate'
import { CreatorShare } from './CreatorShare'
import { Metadata } from './Metadata'
-import { RenderIframe } from './RenderIframe'
import { Footer } from './Footer'
import { BiddersBids } from './BiddersBids'
import { Header } from './Header'
import { ProjectCard } from './ProjectCard'
import { Menu } from './Menu'
+import { MediaObject } from './MediaObject'
+import { NFTE } from './NFTE'
export {
Bidders,
BidCreate,
CreatorShare,
Metadata,
- RenderIframe,
BiddersBids,
Footer,
Header,
Menu,
ProjectCard,
+ MediaObject,
+ NFTE,
}
diff --git a/packages/components/src/stories/MediaObject.stories.tsx b/packages/components/src/stories/MediaObject.stories.tsx
new file mode 100644
index 00000000..184de5e3
--- /dev/null
+++ b/packages/components/src/stories/MediaObject.stories.tsx
@@ -0,0 +1,47 @@
+import type { ComponentStory, ComponentMeta } from '@storybook/react'
+import { MediaObject } from '../MediaObject'
+
+export default {
+ title: 'Components/MediaObject',
+ component: MediaObject,
+ parameters: {
+ layout: 'centered',
+ },
+} as ComponentMeta
+
+const Template: ComponentStory = (args) => (
+
+)
+
+export const Text = Template.bind({})
+Text.args = {
+ mediaURI:
+ 'https://zora-prod.mypinata.cloud/ipfs/bafybeiaevcepa5gwys3ylxtrooxfvv7k2gkxj4kllrid6bofzjdtqrncyi',
+}
+
+export const Video = Template.bind({})
+Video.args = {
+ mediaURI:
+ 'https://d2iccaf7eutw5f.cloudfront.net/0xabEFBc9fD2F806065b4f3C237d4b59D9A97Bcac7/2410/large',
+ width: 300,
+ height: 200,
+ autoPlay: true,
+}
+
+export const Audio = Template.bind({})
+Audio.args = {
+ mediaURI:
+ 'https://d2iccaf7eutw5f.cloudfront.net/0xabEFBc9fD2F806065b4f3C237d4b59D9A97Bcac7/5628/large',
+}
+
+export const Image = Template.bind({})
+Image.args = {
+ mediaURI: 'https://arweave.net/Ds1ggD5l_oDz0k-8jl4yDTaOWDSl9h6G0eq4zIfkf1U',
+ width: 300,
+}
+
+export const Other = Template.bind({})
+Other.args = {
+ mediaURI: 'https://arweave.net/HVFUSMb6D9tUXrYpqsR0mtopiufAkWAueNSqXTRoLyg',
+ width: 300,
+}
diff --git a/packages/components/src/stories/Metadata.stories.tsx b/packages/components/src/stories/Metadata.stories.tsx
index 0fb91ef5..36bea980 100644
--- a/packages/components/src/stories/Metadata.stories.tsx
+++ b/packages/components/src/stories/Metadata.stories.tsx
@@ -13,8 +13,58 @@ const Template: ComponentStory = (args) => (
)
-export const Zero = Template.bind({})
-Zero.args = {
- title: 'dabuk',
- creator: 'beast.near',
+export const Loading = Template.bind({})
+Loading.args = {
+ loading: true,
+ data: {
+ creator_id: 'beast.near',
+ owner_id: 'doge.near',
+ metadata: {
+ title: 'dabuk',
+ description:
+ 'Digital ceramic sculpted in VR and glazed procedurally David Porte Beckefeld® 2021',
+ },
+ },
+}
+
+export const WithDescription = Template.bind({})
+WithDescription.args = {
+ loading: false,
+ data: {
+ creator_id: 'beast.near',
+ owner_id: 'doge.near',
+ metadata: {
+ title: 'dabuk',
+ description:
+ 'Digital ceramic sculpted in VR and glazed procedurally David Porte Beckefeld® 2021',
+ },
+ },
+}
+
+export const WithoutDescription = Template.bind({})
+WithoutDescription.args = {
+ loading: false,
+ data: {
+ creator_id: 'beast.near',
+ owner_id: 'doge.near',
+ metadata: {
+ title: 'dabuk',
+ description: '',
+ },
+ },
+}
+
+export const CustomWidth = Template.bind({})
+CustomWidth.args = {
+ loading: false,
+ width: 500,
+ data: {
+ creator_id: 'beast.near',
+ owner_id: 'doge.near',
+ metadata: {
+ title: 'dabuk',
+ description:
+ 'Digital ceramic sculpted in VR and glazed procedurally David Porte Beckefeld® 2021',
+ },
+ },
}
diff --git a/packages/components/src/stories/NFTE.stories.tsx b/packages/components/src/stories/NFTE.stories.tsx
new file mode 100644
index 00000000..efbee82d
--- /dev/null
+++ b/packages/components/src/stories/NFTE.stories.tsx
@@ -0,0 +1,63 @@
+import type { ComponentStory, ComponentMeta } from '@storybook/react'
+import { NFTE } from '../NFTE'
+
+export default {
+ title: 'Components/NFTE',
+ component: NFTE,
+ parameters: {
+ layout: 'centered',
+ },
+} as ComponentMeta
+
+const Template: ComponentStory = (args) =>
+
+// Hooks that are used currently only support one Network at time
+
+export const Dark = Template.bind({})
+Dark.args = {
+ contract: '0.share-nft.testnet',
+ tokenId: 'afaithraf-68324557',
+ isDark: true,
+}
+
+export const Ml1c = Template.bind({})
+
+Ml1c.args = {
+ contract: 'ml1c.ysn-1_0_0.ysn.testnet',
+ tokenId: 'ysn-63057373',
+}
+
+export const Ml1w = Template.bind({})
+
+Ml1w.args = {
+ contract: 'ml1w.ysn-1_0_0.ysn.testnet',
+ tokenId: 'ghostfrnvpr-62641894',
+}
+
+export const Aprts = Template.bind({})
+
+Aprts.args = {
+ contract: 'apparitions.art-blocks.testnet',
+ tokenId: 'ysn-58907469',
+}
+
+export const Sqgl = Template.bind({})
+
+Sqgl.args = {
+ contract: 'squiggle.art-blocks.testnet',
+ tokenId: 'ysn-62113954',
+}
+
+export const Parasid = Template.bind({})
+
+Parasid.args = {
+ contract: 'x.paras.near',
+ tokenId: '10002:1',
+}
+
+export const Mintbase = Template.bind({})
+
+Mintbase.args = {
+ contract: 'hellovirtualworld.mintbase1.near',
+ tokenId: '2',
+}
diff --git a/packages/components/src/stories/Placeholder.stories.tsx b/packages/components/src/stories/Placeholder.stories.tsx
new file mode 100644
index 00000000..2e699016
--- /dev/null
+++ b/packages/components/src/stories/Placeholder.stories.tsx
@@ -0,0 +1,42 @@
+import type { ComponentStory, ComponentMeta } from '@storybook/react'
+import { Placeholder } from '../Placeholder'
+import { Box } from 'theme-ui'
+
+export default {
+ title: 'Elements/Placeholder',
+ component: Placeholder,
+ parameters: {
+ layout: 'centered',
+ },
+} as ComponentMeta
+
+const Template: ComponentStory = (args) => (
+
+)
+
+export const Default = Template.bind({})
+Default.args = {}
+
+export const Image = Template.bind({})
+Image.args = {
+ width: 300,
+ height: 300,
+}
+
+export const CustomStyles = Template.bind({})
+CustomStyles.args = {
+ width: 80,
+ height: 80,
+ style: {
+ bg: 'primary',
+ borderRadius: '100%',
+ },
+}
+
+export const ParagraphTemplate: ComponentStory = () => (
+
+
+
+
+
+)
diff --git a/packages/components/src/stories/RenderIframe.stories.tsx b/packages/components/src/stories/RenderIframe.stories.tsx
deleted file mode 100644
index 78fa37af..00000000
--- a/packages/components/src/stories/RenderIframe.stories.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { ComponentStory, ComponentMeta } from '@storybook/react'
-import { RenderIframe } from '../RenderIframe'
-
-export default {
- title: 'Components/Render IFrame',
- component: RenderIframe,
- parameters: {
- layout: 'centered',
- },
-} as ComponentMeta
-
-const Template: ComponentStory = (args) => (
-
-)
-
-export const Zero = Template.bind({})
-Zero.args = {
- mediaURI: 'https://arweave.net/H-vPK6GnA5OMZnqpqtirShJT_ELcgPIDShnOr1Z6jmo',
- width: 750,
-}
-
-export const Uno = Template.bind({})
-Uno.args = {
- mediaURI: 'https://arweave.net/HVFUSMb6D9tUXrYpqsR0mtopiufAkWAueNSqXTRoLyg',
- width: 750,
-}
-
-export const Due = Template.bind({})
-Due.args = {
- mediaURI: 'https://arweave.net/LLRlwDUilAxxq4_ZpUkNmolw78UUwTJ2CPJj12jE5As',
- width: 750,
-}
diff --git a/packages/hooks/jest.config.js b/packages/hooks/jest.config.js
new file mode 100644
index 00000000..b49c6c83
--- /dev/null
+++ b/packages/hooks/jest.config.js
@@ -0,0 +1,5 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'jsdom',
+}
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
index af4b3cf5..1a9bd9bf 100644
--- a/packages/hooks/package.json
+++ b/packages/hooks/package.json
@@ -1,6 +1,6 @@
{
"name": "@cura/hooks",
- "version": "0.0.4",
+ "version": "0.0.6",
"description": "Hooks for front-ends on NEAR blockchain",
"source": "src/index.ts",
"main": "dist/index.js",
@@ -11,7 +11,8 @@
"scripts": {
"prepack": "yarn clean && tsc",
"build": "yarn run prepack",
- "clean": "rm -rf ./dist"
+ "clean": "rm -rf ./dist",
+ "test": "jest"
},
"author": "Yassine",
"license": "MIT",
@@ -25,8 +26,13 @@
},
"devDependencies": {
"@babel/core": "^7.14.6",
+ "@testing-library/react-hooks": "^7.0.2",
+ "@types/jest": "^27.0.2",
"babel-loader": "^8.2.2",
+ "jest": "^27.3.1",
+ "jest-fetch-mock": "^3.0.3",
"prettier": "2.3.0",
+ "ts-jest": "^27.0.7",
"typescript": "^4.2.4"
}
}
diff --git a/packages/hooks/src/NFT/useNFT.ts b/packages/hooks/src/NFT/useNFT.ts
new file mode 100644
index 00000000..8bc00c90
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFT.ts
@@ -0,0 +1,27 @@
+import { useNFTViewMethod } from './useNFTViewMethod'
+
+export type useNFTType = {
+ error?: string
+ loading: boolean
+ data?: object
+}
+
+/**
+ * Fetches on-chain NFT metadata for the given ContractAdress and TokenID
+ *
+ * @param contractAddress address of the contract
+ * @param tokenId id of the NFT
+ * @returns {useNFTType} results include loading, error, and on-chain NFT token metadata
+ */
+
+export function useNFT(contractAdress: string, tokenId: string): useNFTType {
+ const NFT = useNFTViewMethod(contractAdress, 'nft_token', {
+ token_id: tokenId,
+ })
+
+ return {
+ error: NFT?.error,
+ loading: NFT.loading,
+ data: NFT?.data,
+ }
+}
diff --git a/packages/hooks/src/NFT/useNFTContentType.ts b/packages/hooks/src/NFT/useNFTContentType.ts
new file mode 100644
index 00000000..0e43c9a4
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFTContentType.ts
@@ -0,0 +1,52 @@
+import useSWR from 'swr'
+
+export type ContentType =
+ | 'image'
+ | 'video'
+ | 'audio'
+ | 'text'
+ | 'html'
+ | 'other'
+
+export type useNFTContentTypeType = {
+ error?: string
+ loading: boolean
+ data?: ContentType
+ content?: string
+}
+
+const fetchContentType = async (URI: string) => {
+ const response = await fetch(URI)
+ const mimeType = await response.headers.get('Content-type')
+
+ let contentType: ContentType = 'other',
+ content: string = ''
+
+ if (mimeType?.includes('image')) contentType = 'image'
+ if (mimeType?.includes('video')) contentType = 'video'
+ if (mimeType?.includes('audio')) contentType = 'audio'
+ if (mimeType?.includes('plain')) contentType = 'text'
+ if (mimeType?.includes('html')) contentType = 'html'
+
+ if (contentType == 'text') content = await response.text()
+
+ return { contentType, content }
+}
+
+/**
+ * Hook to fetch NFT content type from URI,
+ * and returns the content if contentType='text'
+ * @param {string} mediaURI - URI of the NFT media
+ * @returns {useNFTContentTypeType} { error, loading, data, content }
+ */
+
+export function useNFTContentType(mediaURI: string): useNFTContentTypeType {
+ const { data, error } = useSWR([mediaURI], fetchContentType)
+
+ return {
+ error: error,
+ loading: !error && !data,
+ data: data?.contentType,
+ content: data?.content,
+ }
+}
diff --git a/packages/hooks/src/NFT/useNFTContract.ts b/packages/hooks/src/NFT/useNFTContract.ts
new file mode 100644
index 00000000..d7ecd4ca
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFTContract.ts
@@ -0,0 +1,36 @@
+// @ts-nocheck
+import { useEffect, useState } from 'react'
+import { useNearHooksContainer } from '../near'
+import { getContractMethods } from '../near-utils'
+
+/**
+ * Load and returns NFT contract from given ContractAddress
+ *
+ * @param {string} contractAddress - contract adress
+ * @returns {object} { contract }
+ */
+
+export function useNFTContract(
+ contractAddress: string = '0.share-nft.testnet'
+) {
+ const [contract, setContract] = useState({ account: null })
+ const { getContract, accountId } = useNearHooksContainer()
+
+ const setupContract = () => {
+ if (contractAddress.includes('undefined')) {
+ return
+ }
+
+ const newContract = getContract(
+ contractAddress,
+ getContractMethods('nft')
+ )
+
+ setContract(newContract)
+ }
+
+ // If contractName or accountId changes a new contract is setup
+ useEffect(setupContract, [accountId, contractAddress])
+
+ return { contract }
+}
diff --git a/packages/hooks/src/NFT/useNFTContractMetadata.ts b/packages/hooks/src/NFT/useNFTContractMetadata.ts
new file mode 100644
index 00000000..d97bfdcb
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFTContractMetadata.ts
@@ -0,0 +1,26 @@
+import { useNFTViewMethod } from './useNFTViewMethod'
+
+export type useNFTContractMetadataType = {
+ error?: string
+ loading: boolean
+ data?: object
+}
+
+/**
+ * Fetches on-chain NFT contract metadata for the given Contract
+ *
+ * @param contractAddress address of the contract
+ * @returns results include loading, error, and on-chain NFT contract metadata
+ */
+
+export function useNFTContractMetadata(
+ contractAdress: string
+): useNFTContractMetadataType {
+ const NFTMeta = useNFTViewMethod(contractAdress, 'nft_metadata', {})
+
+ return {
+ error: NFTMeta?.error,
+ loading: NFTMeta.loading,
+ data: NFTMeta?.data,
+ }
+}
diff --git a/packages/hooks/src/NFT/useNFTMethod.ts b/packages/hooks/src/NFT/useNFTMethod.ts
new file mode 100644
index 00000000..0bad0f03
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFTMethod.ts
@@ -0,0 +1,45 @@
+// @ts-nocheck
+import { useNFTContract } from './useNFTContract'
+import useSWR from 'swr'
+
+/**
+ * Execute an NFT contract method
+ *
+ * @param {string} contractAddress - contract adress
+ * @param {string} methodName - the view method to execute
+ * @param {object} params - method parameters
+ * @param {number} gas - gas limit
+ * @returns {object} { error, loading, data }
+ */
+
+export function useNFTMethod(
+ contractAddress: string,
+ methodName: string,
+ params: {},
+ gas: number,
+ updateStatus: () => void
+) {
+ const { contract } = useNFTContract(contractAddress)
+
+ const validParams = contract?.account?.accountId
+
+ const fetcher = (methodName: string, serializedParams: string) => {
+ const params = JSON.parse(serializedParams)
+ return contract[methodName]({ ...params }, gas).then((res) => {
+ return res
+ })
+ }
+
+ const { data, error } = useSWR(
+ validParams ? [methodName, JSON.stringify(params)] : null,
+ fetcher
+ )
+
+ updateStatus && updateStatus(error, data, validParams)
+
+ return {
+ data: data,
+ loading: !error && !data,
+ error: error,
+ }
+}
diff --git a/packages/hooks/src/NFT/useNFTReference.ts b/packages/hooks/src/NFT/useNFTReference.ts
new file mode 100644
index 00000000..9ba88b39
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFTReference.ts
@@ -0,0 +1,32 @@
+import useSWR from 'swr'
+
+export type useNFTReferenceType = {
+ error?: string
+ loading: boolean
+ data?: object
+}
+
+const fetchNFTReference = async (URI: string) => {
+ const response = await fetch(URI)
+
+ const data = await response.json()
+ return data
+}
+
+/**
+ * Fetches off-chain NFT metadata from the reference for the given URI
+ *
+ * @param uri NFT reference URI
+ * @returns results include loading, error, and off-chain NFT metadata
+ */
+
+export function useNFTReference(uri: string): useNFTReferenceType {
+ let { data, error } = useSWR(uri ? [uri] : null, fetchNFTReference)
+ error =
+ error || (!uri && 'provided reference URI is not valid') || undefined
+ return {
+ error: error,
+ loading: !error && !data,
+ data: data,
+ }
+}
diff --git a/packages/hooks/src/NFT/useNFTViewMethod.ts b/packages/hooks/src/NFT/useNFTViewMethod.ts
new file mode 100644
index 00000000..68e5f459
--- /dev/null
+++ b/packages/hooks/src/NFT/useNFTViewMethod.ts
@@ -0,0 +1,63 @@
+// @ts-nocheck
+import useSWR from 'swr'
+import { networkId, nodeUrl } from '../near-utils'
+import { connect, Contract } from 'near-api-js'
+import { getContractMethods } from '../near-utils'
+
+export type useNFTViewMethodType = {
+ error?: string
+ loading: boolean
+ data?: any
+}
+
+const fetchNFTView = async (
+ contractAddress: string,
+ methodName: string,
+ serializedParams: string
+) => {
+ const near = await connect({
+ networkId,
+ nodeUrl,
+ deps: {
+ keyStore: undefined,
+ },
+ })
+ const account = await near.account(null)
+
+ const contract = await new Contract(
+ account,
+ contractAddress,
+ getContractMethods('nft')
+ )
+
+ const params = JSON.parse(serializedParams)
+ return await contract[methodName]({ ...params }).then((res) => {
+ return res
+ })
+}
+
+/**
+ * Execute an NFT contract view method without an account
+ *
+ * @param {string} contractAddress - contract adress
+ * @param {string} methodName - the view method to execute
+ * @param {object} params - method parameters
+ * @returns {useNFTViewMethodType} { error, loading, data }
+ */
+
+export function useNFTViewMethod(
+ contractAddress: string,
+ methodName: string,
+ params?: object
+): useNFTViewMethodType {
+ const { data, error } = useSWR(
+ [contractAddress, methodName, JSON.stringify(params || {})],
+ fetchNFTView
+ )
+
+ return {
+ error: error,
+ loading: !error && !data,
+ data: data,
+ }
+}
diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts
index a6bbb33a..3091e7c8 100644
--- a/packages/hooks/src/index.ts
+++ b/packages/hooks/src/index.ts
@@ -1,15 +1,44 @@
-import useNFTContract, { useNFTMethod } from './useNFTContract'
+import { useNFTViewMethod, useNFTViewMethodType } from './NFT/useNFTViewMethod'
+import { useNFT, useNFTType } from './NFT/useNFT'
+import {
+ useNFTContentType,
+ useNFTContentTypeType,
+} from './NFT/useNFTContentType'
+import { useNFTReference, useNFTReferenceType } from './NFT/useNFTReference'
+import {
+ useNFTContractMetadata,
+ useNFTContractMetadataType,
+} from './NFT/useNFTContractMetadata'
+
+import { useNFTContract } from './NFT/useNFTContract'
+import { useNFTMethod } from './NFT/useNFTMethod'
+
import useMarketContract, { useMarketMethod } from './useMarketContract'
import useFTContract, { useFTMethod } from './useFTContract'
import { NearHooksProvider, useNearHooksContainer } from './near'
export {
+ // NFT hooks
+ useNFTViewMethod,
+ useNFT,
+ useNFTContentType,
+ useNFTReference,
useNFTContract,
+ useNFTContractMetadata,
useNFTMethod,
+ // NFT hooks types,
+ useNFTViewMethodType,
+ useNFTType,
+ useNFTContentTypeType,
+ useNFTReferenceType,
+ useNFTContractMetadataType,
+ // Market hooks
useMarketContract,
useMarketMethod,
+ // FT hooks
useFTContract,
useFTMethod,
+ // NEAR hooks
NearHooksProvider,
useNearHooksContainer,
}
diff --git a/packages/hooks/src/near-utils.ts b/packages/hooks/src/near-utils.ts
index 88afe005..bd814a4f 100644
--- a/packages/hooks/src/near-utils.ts
+++ b/packages/hooks/src/near-utils.ts
@@ -9,7 +9,7 @@ function getConfig() {
return config
}
-export const { networkId, nodeUrl, walletUrl } = getConfig()
+export const { nodeUrl, networkId, walletUrl } = getConfig()
export function getContractMethods(contractName: string) {
switch (contractName) {
@@ -32,12 +32,10 @@ export function getContractMethods(contractName: string) {
'claim_media',
'burn_design',
'view_media',
- 'nft_metadata',
- 'nft_token',
'nft_tokens',
'nft_tokens_for_owner',
],
- viewMethods: ['nft_total_supply'],
+ viewMethods: ['nft_total_supply', 'nft_metadata', 'nft_token'],
}
case 'market':
return {
diff --git a/packages/hooks/src/useNFTContract.ts b/packages/hooks/src/useNFTContract.ts
deleted file mode 100644
index 01084f7d..00000000
--- a/packages/hooks/src/useNFTContract.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-// @ts-nocheck
-import { useEffect, useState } from 'react'
-import { useNearHooksContainer } from './near'
-import { getContractMethods } from './near-utils'
-import useSWR from 'swr'
-
-export default function useNFTContract(
- contractAddress: string = '0.share-nft.testnet'
-) {
- const [contract, setContract] = useState({ account: null })
- const { getContract, accountId } = useNearHooksContainer()
-
- const setupContract = () => {
- if (contractAddress.includes('undefined')) {
- return
- }
-
- const newContract = getContract(
- contractAddress,
- getContractMethods('nft')
- )
-
- setContract(newContract)
- }
-
- // If contractName or accountId changes a new contract is setup
- useEffect(setupContract, [accountId, contractAddress])
-
- return { contract }
-}
-
-export const useNFTMethod = (
- contractAddress: string,
- methodName: string,
- params: {},
- gas: number,
- updateStatus: () => void
-) => {
- const { contract } = useNFTContract(contractAddress)
-
- const validParams = contract?.account?.accountId
-
- const fetcher = (methodName: string, serializedParams: string) => {
- const params = JSON.parse(serializedParams)
- return contract[methodName]({ ...params }, gas).then((res) => {
- return res
- })
- }
-
- const { data, error } = useSWR(
- validParams ? [methodName, JSON.stringify(params)] : null,
- fetcher
- )
-
- updateStatus && updateStatus(error, data, validParams)
-
- return {
- data: data,
- loading: !error && !data,
- error: error,
- }
-}
diff --git a/packages/hooks/tests/useNFT.test.ts b/packages/hooks/tests/useNFT.test.ts
new file mode 100644
index 00000000..73ae1fbb
--- /dev/null
+++ b/packages/hooks/tests/useNFT.test.ts
@@ -0,0 +1,23 @@
+import { renderHook, cleanup } from '@testing-library/react-hooks'
+import { useNFT } from '../src/NFT/useNFT'
+import { useNFTViewMethod } from '../src/NFT/useNFTViewMethod'
+
+jest.mock('../src/NFT/useNFTViewMethod')
+
+describe('useNFT', () => {
+ beforeEach(() => {
+ cleanup()
+ jest.resetAllMocks()
+ })
+ it('should execute useNFTViewMethod', () => {
+ renderHook(() => useNFT('0.share-nft.testnet', 'starpause-60610031'))
+
+ expect(useNFTViewMethod).toHaveBeenLastCalledWith(
+ '0.share-nft.testnet',
+ 'nft_token',
+ {
+ token_id: 'starpause-60610031',
+ }
+ )
+ })
+})
diff --git a/packages/hooks/tests/useNFTContentType.test.ts b/packages/hooks/tests/useNFTContentType.test.ts
new file mode 100644
index 00000000..39b330b3
--- /dev/null
+++ b/packages/hooks/tests/useNFTContentType.test.ts
@@ -0,0 +1,30 @@
+import { enableFetchMocks } from 'jest-fetch-mock'
+enableFetchMocks()
+import { renderHook } from '@testing-library/react-hooks'
+import { useNFTContentType } from '../src/NFT/useNFTContentType'
+import { cache } from 'swr'
+
+describe('useNFTContentType', () => {
+ beforeEach(() => {
+ cache.clear()
+ })
+ test('should return contentType from a URI', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useNFTContentType(
+ 'https://arweave.net/mKrJ5JvuMQwKPoxKVYN-q8kMwuF6YnWDEsR2L7G0u-Q'
+ )
+ )
+
+ // loading true, no errors, data is undefined
+ expect(result.current.loading).toBe(true)
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.data).toBeUndefined()
+
+ await waitForNextUpdate()
+
+ // loading complete, no errors, data should be string
+ expect(result.current.loading).toBe(false)
+ expect(result.current.error).toBeUndefined()
+ expect(typeof result.current.data).toEqual('string')
+ })
+})
diff --git a/packages/hooks/tests/useNFTContractMetadata.test.ts b/packages/hooks/tests/useNFTContractMetadata.test.ts
new file mode 100644
index 00000000..b5414831
--- /dev/null
+++ b/packages/hooks/tests/useNFTContractMetadata.test.ts
@@ -0,0 +1,21 @@
+import { renderHook, cleanup } from '@testing-library/react-hooks'
+import { useNFTContractMetadata } from '../src/NFT/useNFTContractMetadata'
+import { useNFTViewMethod } from '../src/NFT/useNFTViewMethod'
+
+jest.mock('../src/NFT/useNFTViewMethod')
+
+describe('useNFTContractMetadata', () => {
+ beforeEach(() => {
+ cleanup()
+ jest.resetAllMocks()
+ })
+ it('should execute useNFTViewMethod', () => {
+ renderHook(() => useNFTContractMetadata('0.share-nft.testnet'))
+
+ expect(useNFTViewMethod).toHaveBeenLastCalledWith(
+ '0.share-nft.testnet',
+ 'nft_metadata',
+ {}
+ )
+ })
+})
diff --git a/packages/hooks/tests/useNFTReference.test.ts b/packages/hooks/tests/useNFTReference.test.ts
new file mode 100644
index 00000000..16eba387
--- /dev/null
+++ b/packages/hooks/tests/useNFTReference.test.ts
@@ -0,0 +1,41 @@
+import { enableFetchMocks } from 'jest-fetch-mock'
+enableFetchMocks()
+import { renderHook } from '@testing-library/react-hooks'
+import { useNFTReference } from '../src/NFT/useNFTReference'
+import { cache } from 'swr'
+import type { FetchMock } from 'jest-fetch-mock'
+const fetchMock = fetch as FetchMock
+
+describe('useNFTReference', () => {
+ beforeEach(() => {
+ cache.clear()
+ })
+ test('should return a Json from a URI', async () => {
+ fetchMock.mockResponseOnce(JSON.stringify({ data: '123' }))
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useNFTReference(
+ 'https://arweave.net/K7oV4o3By4_TV5ItTnmENBR3HS4wrpGZlcDpJYPEpeY'
+ )
+ )
+
+ // loading true, errors undefined, data undefined
+ expect(result.current.loading).toBe(true)
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.data).toBeUndefined()
+
+ await waitForNextUpdate()
+
+ // loading false, no errors, data should be object
+ expect(result.current.loading).toBe(false)
+ expect(result.current.error).toBeUndefined()
+ expect(typeof result.current.data).toEqual('object')
+ })
+ test('should return an error', async () => {
+ const { result } = renderHook(() => useNFTReference(''))
+ // loading false, error exist, data undefined
+ expect(result.current.loading).toBe(false)
+ expect(typeof result.current.error).toEqual('string')
+ expect(result.current.data).toBeUndefined()
+ })
+})
diff --git a/packages/hooks/tests/useNFTViewMethod.test.ts b/packages/hooks/tests/useNFTViewMethod.test.ts
new file mode 100644
index 00000000..e5612b57
--- /dev/null
+++ b/packages/hooks/tests/useNFTViewMethod.test.ts
@@ -0,0 +1,27 @@
+import { renderHook, cleanup } from '@testing-library/react-hooks'
+import { useNFTViewMethod } from '../src/NFT/useNFTViewMethod'
+import { cache } from 'swr'
+
+describe('useNFT', () => {
+ beforeEach(() => {
+ cache.clear()
+ cleanup()
+ })
+ test('should get data from blockchain without any error', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useNFTViewMethod('0.share-nft.testnet', 'nft_total_supply')
+ )
+
+ // loading true, no errors, data is undefined
+ expect(result.current.loading).toBe(true)
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.data).toBeUndefined()
+
+ await waitForNextUpdate()
+
+ // loading complete, no errors, data is an object
+ expect(result.current.loading).toBe(false)
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.data).not.toBeUndefined()
+ })
+})