Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions app/[locale]/resources/_components/resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,24 @@ import { useTranslation } from "@/hooks/useTranslation"
import heroImg from "@/public/images/heroes/guides-hub-hero.jpg"
interface ResourcesPageProps {
txCostsMedianUsd: MetricReturnData
totalBlobs: string
avgBlobFee: number
}

const EVENT_CATEGORY = "dashboard"

const ResourcesPage = ({ txCostsMedianUsd }: ResourcesPageProps) => {
const ResourcesPage = ({
txCostsMedianUsd,
totalBlobs,
avgBlobFee,
}: ResourcesPageProps) => {
const { t } = useTranslation("page-resources")
const resourceSections = useResources({ txCostsMedianUsd })

const resourceSections = useResources({
txCostsMedianUsd,
totalBlobs,
avgBlobFee,
})
const activeSection = useActiveHash(
resourceSections.map(({ key }) => key),
"0% 0% -70% 0%"
Expand Down Expand Up @@ -114,22 +125,22 @@ const ResourcesPage = ({ txCostsMedianUsd }: ResourcesPageProps) => {
{boxes.map(({ title, metric, items, className }) => (
<div
className={cn(
"overflow-hidden rounded-2xl border shadow-lg",
"grid grid-rows-[min-content] overflow-hidden rounded-2xl border shadow-lg",
className
)}
key={title}
>
<div className="border-b bg-[#ffffff] px-6 py-4 font-bold dark:bg-[#171717]">
{title}
</div>
<div className="h-full bg-background bg-gradient-to-br from-white to-primary/10 px-2 py-6 dark:from-transparent dark:to-primary/10">
<Stack className="gap-2 bg-background bg-gradient-to-br from-white to-primary/10 px-2 py-6 dark:from-transparent dark:to-primary/10">
{metric && metric}
<ResourcesContainer>
{items.map((item) => (
<ResourceItem item={item} key={item.title} />
))}
</ResourcesContainer>
</div>
</Stack>
</div>
))}
</div>
Expand Down
22 changes: 19 additions & 3 deletions app/[locale]/resources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import { BASE_TIME_UNIT } from "@/lib/constants"

import ResourcesPage from "./_components/resources"

import { fetchBlobscanStats } from "@/lib/api/fetchBlobscanStats"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"

// In seconds
const REVALIDATE_TIME = BASE_TIME_UNIT * 1

const loadData = dataLoader(
[["growThePieData", fetchGrowThePie]],
[
["growThePieData", fetchGrowThePie],
["blobscanOverallStats", fetchBlobscanStats],
],
REVALIDATE_TIME * 1000
)

Expand All @@ -38,12 +42,24 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
const messages = pick(allMessages, requiredNamespaces)

// Load data
const [growThePieData] = await loadData()
const [growThePieData, blobscanOverallStats] = await loadData()

const { txCostsMedianUsd } = growThePieData

const { totalBlobs, avgBlobFee } = blobscanOverallStats

const formattedTotalBlobs = new Intl.NumberFormat(undefined, {
notation: "compact",
maximumFractionDigits: 1,
}).format(totalBlobs)

return (
<I18nProvider locale={locale} messages={messages}>
<ResourcesPage txCostsMedianUsd={txCostsMedianUsd} />
<ResourcesPage
txCostsMedianUsd={txCostsMedianUsd}
totalBlobs={formattedTotalBlobs}
avgBlobFee={avgBlobFee}
/>
</I18nProvider>
)
}
Expand Down
98 changes: 92 additions & 6 deletions src/components/Resources/useResources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { getLocaleForNumberFormat } from "@/lib/utils/translations"

import BigNumber from "../BigNumber"
import RadialChart from "../RadialChart"
import { BaseLink } from "../ui/Link"

import type { DashboardBox, DashboardSection } from "./types"

import { useEthPrice } from "@/hooks/useEthPrice"
import { useTranslation } from "@/hooks/useTranslation"
import IconBeaconchain from "@/public/images/resources/beaconcha-in.png"
import IconBlobsGuru from "@/public/images/resources/blobsguru.png"
Expand Down Expand Up @@ -54,16 +56,24 @@ const formatSmallUSD = (value: number, locale: string): string =>
new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
notation: "compact",
minimumSignificantDigits: 2,
maximumSignificantDigits: 2,
Comment on lines -57 to -59
Copy link
Contributor Author

@TylerAPfledderer TylerAPfledderer May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify the price formatting for formatSmallUSD(), so that two decimal places are always shown, no matter the number of significant figures. (i.e. $xx.xx versus $x.xx)

}).format(value)

export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
export const useResources = ({
txCostsMedianUsd,
totalBlobs,
avgBlobFee,
}): DashboardSection[] => {
const { t } = useTranslation("page-resources")
const locale = useLocale()
const localeForNumberFormat = getLocaleForNumberFormat(locale! as Lang)

const ethPrice = useEthPrice()
const avgBlobFeeUsd = formatSmallUSD(
// Converting value from wei to USD
avgBlobFee * 1e-18 * ethPrice,
localeForNumberFormat
)

const medianTxCost =
"error" in txCostsMedianUsd
? { error: txCostsMedianUsd.error }
Expand All @@ -74,6 +84,48 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {

const [timeToNextBlock, setTimeToNextBlock] = useState(12)

const [scalingUpgradeCountdown, setPectraCountdown] = useState<string | null>(
"Loading..."
)

useEffect(() => {
// Countdown time for Scaling Upgrade to the final date of May 7 2025
const scalingUpgradeDate = new Date("2025-05-07T00:00:00Z")
const scalingUpgradeDateTime = scalingUpgradeDate.getTime()
const SECONDS = 1000
const MINUTES = SECONDS * 60
const HOURS = MINUTES * 60
const DAYS = HOURS * 24

const countdown = () => {
const now = Date.now()
const timeLeft = scalingUpgradeDateTime - now

// If the date has past, set the countdown to null
if (timeLeft < 0) return setPectraCountdown(null)

const daysLeft = Math.floor(timeLeft / DAYS)
const hoursLeft = Math.floor((timeLeft % DAYS) / HOURS)
const minutesLeft = Math.floor((timeLeft % HOURS) / MINUTES)
const secondsLeft = Math.floor((timeLeft % MINUTES) / SECONDS)

setPectraCountdown(
`${daysLeft}days :: ${hoursLeft}h ${minutesLeft}m ${secondsLeft}s`
)
}
countdown()

let interval: NodeJS.Timeout | undefined

if (scalingUpgradeCountdown !== null) {
// Only run the interval if the date has not passed
interval = setInterval(countdown, SECONDS)
}

return () => clearInterval(interval)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

useEffect(() => {
const genesisTime = new Date("2020-12-01T12:00:23Z").getTime()
const updateTime = () => {
Expand Down Expand Up @@ -350,7 +402,26 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
const scalingBoxes: DashboardBox[] = [
{
title: t("page-resources-roadmap-title"),
// TODO: Add metric
metric: (
<div className="grid place-items-center py-5">
<div className="text-sm">Latest upgrade</div>
<BaseLink
href="/roadmap/pectra/"
className="text-5xl font-bold text-body no-underline hover:text-primary"
>
Pectra
</BaseLink>
<div className="text-xl font-bold text-body-medium">
{scalingUpgradeCountdown ? (
scalingUpgradeCountdown
) : (
<div className="rounded-full bg-success px-2 py-1 text-xs font-normal uppercase text-success-light">
Live Since April 2025
</div>
)}
</div>
</div>
),
items: [
{
title: "Ethereum Roadmap",
Expand All @@ -362,7 +433,22 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
},
{
title: t("page-resources-blobs-title"),
// TODO: Add metric
metric: (
<div className="flex [&>*]:grid [&>*]:flex-1 [&>*]:place-items-center">
<div>
<div className="text-[42px] font-bold leading-2xs">
{totalBlobs}
</div>
<div className="text-sm text-body-medium">Total blobs</div>
</div>
<div>
<div className="text-[42px] font-bold leading-2xs">
{avgBlobFeeUsd}
</div>
<div className="text-sm text-body-medium">Average Blob Fee</div>
</div>
</div>
),
items: [
{
title: "Blob Scan",
Expand Down
18 changes: 18 additions & 0 deletions src/data/mocks/blobscanOverallStats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"avgBlobAsCalldataFee": 18402670294113620,
"avgBlobFee": 1337454615991715,
"avgBlobGasPrice": 4657716809.805255,
"avgMaxBlobGasFee": 19666167416.48503,
"totalBlobGasUsed": "875492278272",
"totalBlobAsCalldataGasUsed": "12165759474144",
"totalBlobFee": "4174952855822794358784",
"totalBlobAsCalldataFee": "57445149899315095107588",
"totalBlobs": 6679476,
"totalBlobSize": "875492278272",
"totalBlocks": 1664933,
"totalTransactions": 3121566,
"totalUniqueBlobs": 6575105,
"totalUniqueReceivers": 5361,
"totalUniqueSenders": 5941,
"updatedAt": "2025-03-25T11:45:00.590Z"
}
57 changes: 57 additions & 0 deletions src/lib/api/fetchBlobscanStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
type BlobscanOverallStats = {
avgBlobAsCalldataFee: number
avgBlobFee: number
avgBlobGasPrice: number
avgMaxBlobGasFee: number
totalBlobGasUsed: string
totalBlobAsCalldataGasUsed: string
totalBlobFee: string
totalBlobAsCalldataFee: string
totalBlobs: number
totalBlobSize: string
totalBlocks: number
totalTransactions: number
totalUniqueBlobs: number
totalUniqueReceivers: number
totalUniqueSenders: number
updatedAt: string
}

type BlobscanOverallStatsErr = {
message: string
code: string
issues: [message: string]
}

/**
* Fetch the overall stats from Blobscan
*
* @see https://api.blobscan.com/#/stats/stats-getOverallStats
*
*/
export const fetchBlobscanStats = async () => {
const data = await fetch("https://api.blobscan.com/stats/overall").then(
(res) => responseHandler(res)
)

return data
}

type BlobscanResponse =
| (Omit<Response, "json"> & {
json: () => BlobscanOverallStats | PromiseLike<BlobscanOverallStats>
})
| (Omit<Response, "json"> & {
json: () => BlobscanOverallStatsErr | PromiseLike<BlobscanOverallStatsErr>
})

const responseHandler = async (response: Response) => {
const res = await (response as BlobscanResponse).json()

if ("message" in res) {
throw Error(`Code ${res.code}: Failed to fetch Blobscan Overall Stats`, {
cause: res.message,
})
}
return res
}