feat: nft page
This commit is contained in:
parent
ddaa1ff6bf
commit
f139375ae7
|
|
@ -10,6 +10,11 @@ stringData:
|
|||
NEXT_PUBLIC_CSB_SCAN: "https://scan.crossbell.io"
|
||||
NEXT_PUBLIC_IPFS_GATEWAY: "https://ipfs.4everland.xyz/ipfs/"
|
||||
REDIS_URL: ${REDIS_URL_DEV}
|
||||
MORALIS_WEB3_API_KEY: ${MORALIS_WEB3_API_KEY}
|
||||
ALCHEMY_ETHEREUM_API_KEY: ${ALCHEMY_ETHEREUM_API_KEY}
|
||||
ALCHEMY_POLYGON_API_KEY: ${ALCHEMY_POLYGON_API_KEY}
|
||||
NFTSCAN_API_KEY: ${NFTSCAN_API_KEY}
|
||||
OPENSEA_API_KEY: ${OPENSEA_API_KEY}
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: xlog
|
||||
|
|
|
|||
|
|
@ -9,3 +9,8 @@ NEXT_PUBLIC_CSB_IO=APP_NEXT_PUBLIC_CSB_IO
|
|||
NEXT_PUBLIC_CSB_SCAN=APP_NEXT_PUBLIC_CSB_SCAN
|
||||
NEXT_PUBLIC_IPFS_GATEWAY=APP_NEXT_PUBLIC_IPFS_GATEWAY
|
||||
REDIS_URL=APP_NEXT_REDIS_URL
|
||||
MORALIS_WEB3_API_KEY=APP_MORALIS_WEB3_API_KEY
|
||||
ALCHEMY_ETHEREUM_API_KEY=APP_ALCHEMY_ETHEREUM_API_KEY
|
||||
ALCHEMY_POLYGON_API_KEY=APP_ALCHEMY_POLYGON_API_KEY
|
||||
NFTSCAN_API_KEY=APP_NFTSCAN_API_KEY
|
||||
OPENSEA_API_KEY=APP_OPENSEA_API_KEY
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ stringData:
|
|||
NEXT_PUBLIC_CSB_SCAN: "https://scan.crossbell.io"
|
||||
NEXT_PUBLIC_IPFS_GATEWAY: "https://ipfs.4everland.xyz/ipfs/"
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
MORALIS_WEB3_API_KEY: ${MORALIS_WEB3_API_KEY}
|
||||
ALCHEMY_ETHEREUM_API_KEY: ${ALCHEMY_ETHEREUM_API_KEY}
|
||||
ALCHEMY_POLYGON_API_KEY: ${ALCHEMY_POLYGON_API_KEY}
|
||||
NFTSCAN_API_KEY: ${NFTSCAN_API_KEY}
|
||||
OPENSEA_API_KEY: ${OPENSEA_API_KEY}
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: xlog
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
"remark-stringify": "10.0.2",
|
||||
"sharp": "0.31.2",
|
||||
"sortablejs": "1.15.0",
|
||||
"unidata.js": "0.7.7",
|
||||
"unidata.js": "0.7.9",
|
||||
"unified": "10.1.2",
|
||||
"unist-builder": "3.0.0",
|
||||
"unist-util-visit": "4.1.1",
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ specifiers:
|
|||
sortablejs: 1.15.0
|
||||
tailwindcss: 3.2.4
|
||||
typescript: 4.9.3
|
||||
unidata.js: 0.7.7
|
||||
unidata.js: 0.7.9
|
||||
unified: 10.1.2
|
||||
unist-builder: 3.0.0
|
||||
unist-util-visit: 4.1.1
|
||||
|
|
@ -175,7 +175,7 @@ dependencies:
|
|||
remark-stringify: 10.0.2
|
||||
sharp: 0.31.2
|
||||
sortablejs: 1.15.0
|
||||
unidata.js: 0.7.7
|
||||
unidata.js: 0.7.9
|
||||
unified: 10.1.2
|
||||
unist-builder: 3.0.0
|
||||
unist-util-visit: 4.1.1
|
||||
|
|
@ -3361,8 +3361,8 @@ packages:
|
|||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/crossbell.js/0.10.40:
|
||||
resolution: {integrity: sha512-kGuAVKJA9PyYISGJRSmUmsInm2mN2y6p5pnO4cAAOPtiBI+uicXPAk+4FFS1sykYMyO0Iqc4gWycK1254apqOw==}
|
||||
/crossbell.js/0.11.1:
|
||||
resolution: {integrity: sha512-mAYlD05rqVKN12xYJW8MgCWrLiWMMgxyGXb6OOCvnpMtCKkRPKHjHAOPd/X07GwqVj2kZQ+0f5/xccsn1M3V5A==}
|
||||
dependencies:
|
||||
async-retry: 1.3.3
|
||||
ethers: 5.7.2
|
||||
|
|
@ -3378,8 +3378,8 @@ packages:
|
|||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/crossbell.js/0.11.1:
|
||||
resolution: {integrity: sha512-mAYlD05rqVKN12xYJW8MgCWrLiWMMgxyGXb6OOCvnpMtCKkRPKHjHAOPd/X07GwqVj2kZQ+0f5/xccsn1M3V5A==}
|
||||
/crossbell.js/0.11.5:
|
||||
resolution: {integrity: sha512-25lvV2YVNpgb7mZapJAO429ToYuFvx7f2I4/MM13YP91YtvABWoiBrne/gHIyXlEb6QqclQNVRY1H63Vhdabrg==}
|
||||
dependencies:
|
||||
async-retry: 1.3.3
|
||||
ethers: 5.7.2
|
||||
|
|
@ -8401,14 +8401,14 @@ packages:
|
|||
dependencies:
|
||||
busboy: 1.6.0
|
||||
|
||||
/unidata.js/0.7.7:
|
||||
resolution: {integrity: sha512-RPUA4r6nf63TNeuFaY1CljfHOly1iIWFmjS+UC8h4ew/gWgTzBKxqOLRRk9+0tiorOzjyoysJsvQIoNn/BRc+A==}
|
||||
/unidata.js/0.7.9:
|
||||
resolution: {integrity: sha512-QSJGVGAgN+5KJA8wio0Yith/3ukSd8kVdSUDx9IYE0r7qMkNiasVtzVdx1appGADRR2XWnHeeLjZ/5n8cfemuQ==}
|
||||
dependencies:
|
||||
'@types/lodash': 4.14.190
|
||||
'@urql/core': 3.0.5_graphql@16.6.0
|
||||
axios: 1.1.3
|
||||
axios-retry: 3.3.1
|
||||
crossbell.js: 0.10.40
|
||||
crossbell.js: 0.11.5
|
||||
ethers: 5.7.2
|
||||
graphql: 16.6.0
|
||||
lodash: 4.17.21
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const getServerSideProps = async (
|
|||
options?: {
|
||||
take?: number
|
||||
useStat?: boolean
|
||||
skipPages?: boolean
|
||||
},
|
||||
) => {
|
||||
const domainOrSubdomain = ctx.params!.site as string
|
||||
|
|
@ -61,19 +62,21 @@ export const getServerSideProps = async (
|
|||
reject(error)
|
||||
}
|
||||
} else {
|
||||
await prefetchGetPagesBySite(
|
||||
{
|
||||
site: domainOrSubdomain,
|
||||
...(options?.take && { take: options.take }),
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
...(tag && { tags: [tag] }),
|
||||
...(options?.useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
if (!options?.skipPages) {
|
||||
await prefetchGetPagesBySite(
|
||||
{
|
||||
site: domainOrSubdomain,
|
||||
...(options?.take && { take: options.take }),
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
...(tag && { tags: [tag] }),
|
||||
...(options?.useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
}
|
||||
resolve(null)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const Image: React.FC<
|
|||
src?: string
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
"original-src"?: string
|
||||
} & React.HTMLAttributes<HTMLImageElement> &
|
||||
ImageProps
|
||||
> = ({ fill, className, alt, src, width, height, ...props }) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import React, { useState } from "react"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import axios from "axios"
|
||||
import { IPFS_GATEWAY } from "~/lib/env"
|
||||
|
||||
export const UniMedia: React.FC<{
|
||||
src: string
|
||||
mime_type?: string
|
||||
}> = ({ src, mime_type }) => {
|
||||
const [errorHandled, setErrorHandled] = useState(false)
|
||||
|
||||
const [type, setType] = useState(mime_type)
|
||||
|
||||
const handleError = async (e: any) => {
|
||||
const src = e.target.getAttribute("original-src")
|
||||
if (src && !errorHandled) {
|
||||
try {
|
||||
const result = await axios({
|
||||
url: src.replace(IPFS_GATEWAY, "https://gateway.ipfs.io/ipfs/"),
|
||||
method: "HEAD",
|
||||
})
|
||||
setType(result.headers["content-type"])
|
||||
setErrorHandled(true)
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-0 pt-[100%] relative rounded overflow-hidden">
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0">
|
||||
{(() => {
|
||||
switch (type?.split("/")[0]) {
|
||||
case "video":
|
||||
return (
|
||||
<video
|
||||
className="w-full h-full"
|
||||
src={src}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
)
|
||||
case "model":
|
||||
return (
|
||||
// @ts-ignore
|
||||
<model-viewer
|
||||
class="w-full h-full"
|
||||
src={src}
|
||||
ar
|
||||
ar-modes="webxr scene-viewer quick-look"
|
||||
seamless-poster
|
||||
shadow-intensity="1"
|
||||
camera-controls
|
||||
enable-pan
|
||||
/>
|
||||
)
|
||||
case "text":
|
||||
return <iframe className="w-full h-full" src={src}></iframe>
|
||||
default:
|
||||
return (
|
||||
<Image
|
||||
className="object-cover"
|
||||
alt={"nft"}
|
||||
src={src}
|
||||
original-src={src}
|
||||
fill={true}
|
||||
onError={handleError}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -19,3 +19,9 @@ export const REDIS_URL = process.env.REDIS_URL
|
|||
export const REDIS_EXPIRE =
|
||||
parseInt(process.env.REDIS_URL || "0") || 60 * 60 * 24 * 7 // 1 week
|
||||
export const REDIS_REFRESH = parseInt(process.env.REDIS_URL || "0") || 5 * 1000 // 5 seconds
|
||||
|
||||
export const MORALIS_WEB3_API_KEY = process.env.MORALIS_WEB3_API_KEY
|
||||
export const ALCHEMY_ETHEREUM_API_KEY = process.env.ALCHEMY_ETHEREUM_API_KEY
|
||||
export const ALCHEMY_POLYGON_API_KEY = process.env.ALCHEMY_POLYGON_API_KEY
|
||||
export const NFTSCAN_API_KEY = process.env.NFTSCAN_API_KEY
|
||||
export const OPENSEA_API_KEY = process.env.OPENSEA_API_KEY
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ if (REDIS_URL) {
|
|||
})
|
||||
}
|
||||
|
||||
export const getRedis = () => redis
|
||||
|
||||
export async function cacheGet(
|
||||
key: string | (Record<string, any> | string | undefined)[],
|
||||
getValueFun: () => Promise<any>,
|
||||
|
|
|
|||
|
|
@ -484,3 +484,11 @@ export async function removeOperator(
|
|||
return contract?.removeOperator(input.characterId, input.operator)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNFTs(address: string, customUnidata?: Unidata) {
|
||||
const assets = await (customUnidata || unidata).assets.get({
|
||||
source: "Ethereum NFT",
|
||||
identity: address,
|
||||
})
|
||||
return assets
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { SiteLayout } from "~/components/site/SiteLayout"
|
||||
import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
|
||||
import { dehydrate, QueryClient } from "@tanstack/react-query"
|
||||
import { useGetSite, useGetNFTs } from "~/queries/site"
|
||||
import { ReactElement, useEffect, useState } from "react"
|
||||
import type { Asset } from "unidata.js"
|
||||
import Script from "next/script"
|
||||
import { UniMedia } from "~/components/ui/UniMedia"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const queryClient = new QueryClient()
|
||||
const domainOrSubdomain = ctx.params!.site as string
|
||||
await getLayoutServerSideProps(ctx, queryClient, {
|
||||
skipPages: true,
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
|
||||
domainOrSubdomain,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function SiteNFTPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
|
||||
const nftsOrigin = useGetNFTs(site.data?.metadata?.owner)
|
||||
|
||||
const [nfts, setNfts] = useState<Asset[]>([])
|
||||
useEffect(() => {
|
||||
if (nftsOrigin?.data?.list && !nfts.length) {
|
||||
setNfts(nftsOrigin.data.list)
|
||||
}
|
||||
}, [nftsOrigin.data, nfts])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
type="module"
|
||||
src="https://cdn.jsdelivr.net/npm/@google/model-viewer/dist/model-viewer.min.js"
|
||||
></Script>
|
||||
<h2 className="text-xl font-bold page-title">NFT Showcase</h2>
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-10 mt-8">
|
||||
{nfts
|
||||
.filter((nft) => nft.items?.[0]?.address)
|
||||
.map((nft: Asset) => (
|
||||
<UniLink
|
||||
key={nft.metadata?.proof}
|
||||
className="xlog-nft flex items-center flex-col"
|
||||
href={nft.related_urls?.[nft.related_urls.length - 1]}
|
||||
data-collection={nft.metadata?.collection_address}
|
||||
data-network={nft.metadata?.network}
|
||||
data-token-id={nft.metadata?.token_id}
|
||||
data-name={nft.name}
|
||||
>
|
||||
<UniMedia
|
||||
src={nft.items?.[0]?.address || ""}
|
||||
mime_type={nft.items?.[0].mime_type}
|
||||
/>
|
||||
<div className="text-xs mt-2 text-center font-medium">
|
||||
{nft.name}
|
||||
</div>
|
||||
</UniLink>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SiteNFTPage.getLayout = (page: ReactElement) => {
|
||||
return <SiteLayout>{page}</SiteLayout>
|
||||
}
|
||||
|
||||
export default SiteNFTPage
|
||||
|
|
@ -23,7 +23,6 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
},
|
||||
queryClient,
|
||||
)
|
||||
console.log(pages)
|
||||
|
||||
const link = getSiteLink({
|
||||
subdomain: site.username || "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { getNFTs } from "~/models/site.model"
|
||||
import { getRedis, cacheGet } from "~/lib/redis.server"
|
||||
import { Asset } from "unidata.js"
|
||||
import { IPFS_GATEWAY } from "~/lib/env"
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const query = req.query
|
||||
|
||||
if (!query.address) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
const redis = getRedis()
|
||||
const redisKey = `nfts/${query.address}`
|
||||
|
||||
const cache = await redis.get(redisKey)
|
||||
if (cache) {
|
||||
res.status(200).json(JSON.parse(cache))
|
||||
} else {
|
||||
const result = await getNFTs(query.address as string)
|
||||
await Promise.all(
|
||||
result.list.map(async (nft: Asset) => {
|
||||
if (!nft.items?.[0].mime_type && nft.items?.[0]?.address) {
|
||||
try {
|
||||
const mime_type = await cacheGet(
|
||||
`nft-mimetype/${nft.items[0].address}`,
|
||||
async () => {
|
||||
const head = await fetch(
|
||||
`${nft.items![0].address!.replace(
|
||||
IPFS_GATEWAY,
|
||||
"https://gateway.ipfs.io/ipfs/",
|
||||
)}`,
|
||||
{
|
||||
method: "HEAD",
|
||||
},
|
||||
)
|
||||
return head.headers.get("content-type")
|
||||
},
|
||||
)
|
||||
nft.items[0].mime_type = mime_type
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
return nft
|
||||
}),
|
||||
)
|
||||
redis.set(redisKey, JSON.stringify(result), "EX", 60 * 60 * 24)
|
||||
res.status(200).json(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -264,3 +264,19 @@ export function useRemoveOperator() {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
export const useGetNFTs = (address: string) => {
|
||||
return useQuery(["getNFTs", address], async () => {
|
||||
if (!address) {
|
||||
return null
|
||||
}
|
||||
return await (
|
||||
await fetch(
|
||||
"/api/nfts?" +
|
||||
new URLSearchParams({
|
||||
address,
|
||||
} as any),
|
||||
)
|
||||
).json()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
import Unidata from "unidata.js"
|
||||
import { IPFS_GATEWAY } from "../lib/env"
|
||||
import {
|
||||
MORALIS_WEB3_API_KEY,
|
||||
ALCHEMY_ETHEREUM_API_KEY,
|
||||
ALCHEMY_POLYGON_API_KEY,
|
||||
NFTSCAN_API_KEY,
|
||||
OPENSEA_API_KEY,
|
||||
} from "../lib/env.server"
|
||||
|
||||
export default new Unidata({
|
||||
ipfsGateway: IPFS_GATEWAY,
|
||||
moralisWeb3APIKey: MORALIS_WEB3_API_KEY,
|
||||
alchemyEthereumAPIKey: ALCHEMY_ETHEREUM_API_KEY,
|
||||
alchemyPolygonAPIKey: ALCHEMY_POLYGON_API_KEY,
|
||||
nftscanAPIKey: NFTSCAN_API_KEY,
|
||||
openseaAPIKey: OPENSEA_API_KEY,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const useUnidata = () => {
|
|||
connector?.getProvider().then((provider) => {
|
||||
unidata = new Unidata({
|
||||
ethereumProvider: provider,
|
||||
ipfsGateway: IPFS_GATEWAY,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue