Skip to content

Commit a3b6116

Browse files
feat: fetch and calculate stats from NFTPort (#2085)
* feat: fetch and calculate stats from NFTPort * fix: mobile layout for lg down * feat: update NFTPort stat calculations * chore: adjust responsive font sizes * feat: add eth conversion fallback and refactor into function * feat: add missing nft values to total * chore: update nftport stats calclations * chore: update fallback NFTPort data * chore: add minus sign to drops in activity * chore: update stats text colors
1 parent db094ca commit a3b6116

5 files changed

Lines changed: 210 additions & 8 deletions

File tree

packages/website/.env.development

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ NEXT_PUBLIC_SENTRY_DSN=https://000000@0000000.ingest.sentry.io/00000
22
NEXT_PUBLIC_ENV=dev
33
NEXT_PUBLIC_API=http://localhost:8787
44
NEXT_PUBLIC_MAGIC=<PUBLISHABLE_API_KEY>
5-
NEXT_PUBLIC_STATUS_PAGE_API=https://nftstorage-jroach.statuspage.io/api/v2
5+
NEXT_PUBLIC_STATUS_PAGE_API=
6+
7+
# NFTPort API
8+
NEXT_PUBLIC_NFT_PORT_ENDPOINT=https://api.nftport.xyz/v0/reports/uris
9+
# Get API Key at https://dashboard.nftport.xyz/sign-up
10+
NEXT_PUBLIC_NFT_PORT_API_KEY=

packages/website/lib/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
let API = /** @type {string} **/ process.env.NEXT_PUBLIC_API || ''
22
let MAGIC_TOKEN = /** @type {string} **/ process.env.NEXT_PUBLIC_MAGIC || ''
3+
export const NFT_PORT_ENDPOINT =
4+
/** @type {string} **/ process.env.NEXT_PUBLIC_NFT_PORT_ENDPOINT ||
5+
'https://api.nftport.xyz/v0/reports/uris'
6+
export const NFT_PORT_API_KEY =
7+
/** @type {string} **/ process.env.NEXT_PUBLIC_NFT_PORT_API_KEY || ''
38

49
if (globalThis.window) {
510
switch (location.host) {
@@ -33,4 +38,6 @@ export default {
3338
AUTHENTICATED_ROUTES,
3439
API: API,
3540
MAGIC_TOKEN: MAGIC_TOKEN,
41+
NFT_PORT_ENDPOINT: NFT_PORT_ENDPOINT,
42+
NFT_PORT_API_KEY: NFT_PORT_API_KEY,
3643
}

packages/website/lib/statsUtils.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ const getTotalUploads = (stats, keys) => {
4242
const getPreviousTotal = (stats, totalUploads) =>
4343
totalUploads - stats.uploads_past_7_total
4444

45+
const getRateFromEthTo = async (to = 'USD') => {
46+
const ethRateAPI = `https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=${to}`
47+
return await (await fetch(ethRateAPI, { method: 'GET' })).json().catch(() => {
48+
return null
49+
})
50+
}
51+
4552
/**
4653
* Format Bytes
4754
* @param {any} stats
@@ -63,3 +70,60 @@ export function calculateStats(stats) {
6370

6471
return { ...stats, totalUploads, growthRate, dealsSizeGrowthRate }
6572
}
73+
74+
/**
75+
* Aggregate stats from NFTPort
76+
* @param {any} stats
77+
* @returns {Promise<any>}
78+
*/
79+
export async function calculateMarketStats(stats) {
80+
const { USD: ethConversion } = await getRateFromEthTo('USD')
81+
const statsToCalculate = ['image_uri_stats']
82+
const calculated = Object.keys(stats)
83+
.filter((stat) => statsToCalculate.includes(stat))
84+
.reduce(
85+
(a, c) => {
86+
for (const prop in stats[c]) {
87+
a.totalNfts += stats[c][prop].count_successful
88+
a.totalNfts += stats[c][prop].count_failed
89+
a.totalMissing += stats[c][prop].count_failed
90+
a.missingMarketValue += stats[c][prop].estimated_price_failed
91+
a.totalMarketValue += stats[c][prop].estimated_price_successful
92+
a.totalMarketValue += stats[c][prop].estimated_price_failed
93+
}
94+
95+
return a
96+
},
97+
{
98+
totalNfts: 0,
99+
totalMarketValue: 0.0,
100+
totalMissing: 0,
101+
missingMarketValue: 0.0,
102+
missingPercentage: 0.0,
103+
totalMarketValueUSD: 0,
104+
missingMarketValueUSD: 0,
105+
}
106+
)
107+
calculated.totalNfts = parseFloat(calculated.totalNfts.toFixed(0))
108+
calculated.totalMarketValue = parseFloat(
109+
calculated.totalMarketValue.toFixed(2)
110+
)
111+
calculated.missingMarketValue = parseFloat(
112+
calculated.missingMarketValue.toFixed(2)
113+
)
114+
if (ethConversion) {
115+
calculated.totalMarketValueUSD =
116+
parseFloat(calculated.totalMarketValue.toFixed(2)) *
117+
parseFloat(ethConversion.toFixed(2))
118+
calculated.missingMarketValueUSD =
119+
parseFloat(calculated.missingMarketValue.toFixed(2)) *
120+
parseFloat(ethConversion.toFixed(2))
121+
}
122+
calculated.missingPercentage = (() => {
123+
return parseFloat(
124+
((calculated.totalMissing / calculated.totalNfts) * 100).toFixed(1)
125+
)
126+
})()
127+
128+
return calculated
129+
}

packages/website/pages/stats.js

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { useEffect, useState } from 'react'
22
import { TrustedBy } from '../components/trustedByLogos'
33
import { NftUpCta } from '../components/nftUpCta'
44
import fs from 'fs'
5-
import { calculateStats } from '../lib/statsUtils'
5+
import { calculateStats, calculateMarketStats } from '../lib/statsUtils'
66
import Img from '../components/cloudflareImage'
77
import { API } from '../lib/api'
88
import Loading from '../components/loading'
99
import bytes from 'bytes'
10+
import { NFT_PORT_ENDPOINT, NFT_PORT_API_KEY } from '../lib/constants'
1011

1112
/**
1213
*
@@ -47,6 +48,8 @@ export function getStaticProps() {
4748
export default function Stats({ logos }) {
4849
/** @type [any, any] */
4950
const [stats, setStats] = useState({})
51+
/** @type [any, any] */
52+
const [marketStats, setMarketStats] = useState({})
5053
const [statsLoading, setStatsLoading] = useState(false)
5154

5255
useEffect(() => {
@@ -55,6 +58,29 @@ export default function Stats({ logos }) {
5558

5659
async function fetchStats() {
5760
setStatsLoading(true)
61+
try {
62+
const nftPortStats = await fetch(NFT_PORT_ENDPOINT, {
63+
method: 'GET',
64+
headers: {
65+
'Content-Type': 'application/json',
66+
Authorization: NFT_PORT_API_KEY,
67+
},
68+
redirect: 'follow',
69+
})
70+
const data = await nftPortStats.json()
71+
setMarketStats(await calculateMarketStats(data.report))
72+
} catch (e) {
73+
const fakeData = {
74+
totalNfts: 91100000,
75+
totalMarketValueUSD: 26800000000,
76+
totalMarketValue: 0,
77+
totalMissing: 1150000,
78+
missingPercentage: 22.3,
79+
missingMarketValueUSD: 874700000,
80+
missingMarketValue: 0,
81+
}
82+
setMarketStats(fakeData)
83+
}
5884
try {
5985
const stats = await fetch(`${API}/stats`, {
6086
method: 'GET',
@@ -110,6 +136,20 @@ export default function Stats({ logos }) {
110136
)
111137
}
112138

139+
/**
140+
* @param {Object} props
141+
* @param {string} [props.title]
142+
* @param {any} [props.children]
143+
*/
144+
const MarketStatCard = ({ title, children }) => {
145+
return (
146+
<div className="market-stats-card bg-white text-center border border-black h-full box-content flex flex-col justify-center p-4">
147+
<div>{children}</div>
148+
<div className="text-lg">{title}</div>
149+
</div>
150+
)
151+
}
152+
113153
const StatCards = () => {
114154
const figureClass = `chicagoflf text-[clamp(22px,_4.2rem,_6vw)] my-5`
115155
const statImageClass = `w-full border-b border-black object-cover aspect-[5/2]`
@@ -140,9 +180,11 @@ export default function Stats({ logos }) {
140180
</figure>
141181
<p
142182
className={`chicagoflf ${
143-
stats.growthRate > 0
144-
? `text-forest before:content-['+']`
145-
: 'text-red'
183+
stats.growthRate >= 0
184+
? `text-forest ${
185+
stats.growthRate > 0 ? "before:content-['+']" : ''
186+
}`
187+
: `text-red before:content-['-']`
146188
}`}
147189
>
148190
{stats.growthRate || 0}%
@@ -172,9 +214,11 @@ export default function Stats({ logos }) {
172214
</figure>
173215
<p
174216
className={`chicagoflf ${
175-
stats.deals_total > 0
176-
? `text-forest before:content-['+']`
177-
: 'text-red'
217+
stats.deals_total >= 0
218+
? `text-forest ${
219+
stats.deals_total > 0 ? "before:content-['+']" : ''
220+
}`
221+
: `text-red before:content-['-']`
178222
}`}
179223
>
180224
{stats.dealsSizeGrowthRate || 0}%
@@ -189,10 +233,78 @@ export default function Stats({ logos }) {
189233
)
190234
}
191235

236+
const MarketStatCards = () => {
237+
return (
238+
<div className="max-w-7xl mx-auto py-4 px-6 sm:px-16">
239+
<div className="mb-16 pl-8 grid gap-x-4 gap-y-[8vw] md:grid-cols-2 xl:grid-cols-4">
240+
<MarketStatCard title="Total Count of NFTS">
241+
<figure className="chicagoflf text-[clamp(2rem,2.6rem,3.3rem)] text-navy">
242+
{statsLoading && <Loading />}
243+
{new Intl.NumberFormat('en-GB', {
244+
notation: 'compact',
245+
compactDisplay: 'short',
246+
maximumFractionDigits: 1,
247+
}).format(marketStats.totalNfts || 0)}
248+
</figure>
249+
</MarketStatCard>
250+
<MarketStatCard title="Total market value of NFTs">
251+
<figure className="chicagoflf text-[clamp(2rem,2.6rem,3.3rem)] text-forest">
252+
{statsLoading && <Loading />}
253+
{marketStats.totalMarketValueUSD > 0 ? '$' : 'Ξ'}
254+
{new Intl.NumberFormat('en-GB', {
255+
notation: 'compact',
256+
compactDisplay: 'short',
257+
maximumFractionDigits: 1,
258+
}).format(
259+
marketStats.missingMarketValueUSD > 0
260+
? marketStats.totalMarketValueUSD
261+
: marketStats.totalMarketValue || 0
262+
)}
263+
</figure>
264+
</MarketStatCard>
265+
<MarketStatCard title="Market value of missing NFTs">
266+
<figure className="chicagoflf text-[clamp(2rem,2.6rem,3.3rem)] text-red">
267+
{statsLoading && <Loading />}
268+
{marketStats.missingMarketValueUSD > 0 ? '$' : 'Ξ'}
269+
{new Intl.NumberFormat('en-GB', {
270+
notation: 'compact',
271+
compactDisplay: 'short',
272+
maximumFractionDigits: 1,
273+
}).format(
274+
marketStats.missingMarketValueUSD > 0
275+
? marketStats.missingMarketValueUSD
276+
: marketStats.missingMarketValue || 0
277+
)}
278+
</figure>
279+
</MarketStatCard>
280+
<MarketStatCard title="Percentage of NFTs deemed missing">
281+
<figure className="chicagoflf text-[clamp(2rem,2.6rem,3.3rem)] text-yellow">
282+
{statsLoading && <Loading />}
283+
{marketStats.missingPercentage ?? 0}%
284+
</figure>
285+
</MarketStatCard>
286+
</div>
287+
</div>
288+
)
289+
}
290+
192291
return (
193292
<main className="bg-nsgreen">
194293
<Marquee />
195294
<StatCards />
295+
<div className="bg-nspeach">
296+
<div className="relative w-screen flex items-center justify-center">
297+
<div className="text-center">
298+
<p className="chicagoflf p-4 m-0 mt-5 text-[clamp(14px,_2rem,_6vw)]">
299+
NFT Market By the Numbers
300+
</p>
301+
<p className="chicagoflf p-4 m-0 mt-5 text-[clamp(12px,_1.6rem,_6vw)]">
302+
The Price of Missing NFTS
303+
</p>
304+
</div>
305+
</div>
306+
<MarketStatCards />
307+
</div>
196308
<div className="bg-nsblue">
197309
<div className="stats-trusted-wrapper max-w-7xl mx-auto py-4 px-6 sm:px-16">
198310
<div>

packages/website/styles/stats.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@
1717
transform: translate3d(1rem, -1rem, 0);
1818
}
1919

20+
.market-stats-card {
21+
box-sizing: border-box;
22+
z-index: 1;
23+
position: relative;
24+
padding: 2rem;
25+
margin: 0 0.25rem;
26+
box-shadow: black 8px 8px 0 0;
27+
border-radius: 2px;
28+
}
29+
30+
.market-stats-card:nth-child(even) {
31+
margin-top: 1rem;
32+
}
33+
2034
.stats-trusted-wrapper > div {
2135
max-width: calc(100% - 2rem);
2236
margin: 4rem 0;

0 commit comments

Comments
 (0)