diff --git a/app/[locale]/resources/_components/ResourcesNav.tsx b/app/[locale]/resources/_components/ResourcesNav.tsx
new file mode 100644
index 00000000000..f1830441d8a
--- /dev/null
+++ b/app/[locale]/resources/_components/ResourcesNav.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import { motion } from "framer-motion"
+
+import { ButtonLink } from "@/components/ui/buttons/Button"
+
+import { cn } from "@/lib/utils/cn"
+
+import type { DashboardSection } from "../types"
+
+import { useActiveHash } from "@/hooks/useActiveHash"
+
+const ResourcesNav = ({
+ resourceSections,
+ eventCategory,
+}: {
+ resourceSections: DashboardSection[]
+ eventCategory: string
+}) => {
+ const activeSection = useActiveHash(
+ resourceSections.map(({ key }) => key),
+ "0% 0% -70% 0%"
+ ).replace(/^#/, "")
+
+ return (
+
+ {resourceSections.map(({ key, title, icon }) => (
+
+ {activeSection === key && (
+
+ )}
+ {icon && {icon} }
+ {title}
+
+ ))}
+
+ )
+}
+
+export default ResourcesNav
diff --git a/src/components/Resources/index.tsx b/app/[locale]/resources/_components/ResourcesUI.tsx
similarity index 94%
rename from src/components/Resources/index.tsx
rename to app/[locale]/resources/_components/ResourcesUI.tsx
index ac94a5202af..6dd3af281bf 100644
--- a/src/components/Resources/index.tsx
+++ b/app/[locale]/resources/_components/ResourcesUI.tsx
@@ -3,9 +3,8 @@ import { Tag } from "@/components/ui/tag"
import { cn } from "@/lib/utils/cn"
-import { Image } from "../Image"
-
-import { Item } from "./types"
+import { Image } from "../../../../src/components/Image"
+import { Item } from "../types"
export const DashboardBox = ({
className,
diff --git a/app/[locale]/resources/_components/SlotCountdown.tsx b/app/[locale]/resources/_components/SlotCountdown.tsx
new file mode 100644
index 00000000000..fbbfaf587a2
--- /dev/null
+++ b/app/[locale]/resources/_components/SlotCountdown.tsx
@@ -0,0 +1,40 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { useLocale } from "next-intl"
+
+import RadialChart from "@/components/RadialChart"
+
+const SlotCountdownChart = ({ children }: { children: string }) => {
+ const [timeToNextBlock, setTimeToNextBlock] = useState(12)
+ const locale = useLocale()
+
+ useEffect(() => {
+ const genesisTime = new Date("2020-12-01T12:00:23Z").getTime()
+ const updateTime = () => {
+ const now = Date.now()
+ const timeElapsed = (now - genesisTime) / 1000
+ const timeToNext = 12 - (timeElapsed % 12)
+ setTimeToNextBlock(Math.floor(timeToNext) || 12)
+ }
+
+ updateTime()
+ const interval = setInterval(updateTime, 1000)
+ return () => clearInterval(interval)
+ }, [])
+
+ return (
+
+ )
+}
+
+export default SlotCountdownChart
diff --git a/app/[locale]/resources/_components/resources.tsx b/app/[locale]/resources/_components/resources.tsx
deleted file mode 100644
index 11dd09d0c68..00000000000
--- a/app/[locale]/resources/_components/resources.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-"use client"
-
-import { motion } from "framer-motion"
-import { FaGithub } from "react-icons/fa6"
-
-import type { MetricReturnData } from "@/lib/types"
-
-import BannerNotification from "@/components/Banners/BannerNotification"
-import { HubHero } from "@/components/Hero"
-import StackIcon from "@/components/icons/stack.svg"
-import MainArticle from "@/components/MainArticle"
-import { ResourceItem, ResourcesContainer } from "@/components/Resources"
-import { useResources } from "@/components/Resources/useResources"
-import Translation from "@/components/Translation"
-import { ButtonLink } from "@/components/ui/buttons/Button"
-import { Stack, VStack } from "@/components/ui/flex"
-import Link from "@/components/ui/Link"
-import { Section } from "@/components/ui/section"
-
-import { cn } from "@/lib/utils/cn"
-
-import { GITHUB_REPO_URL } from "@/lib/constants"
-
-import { useActiveHash } from "@/hooks/useActiveHash"
-import { useTranslation } from "@/hooks/useTranslation"
-import heroImg from "@/public/images/heroes/guides-hub-hero.jpg"
-interface ResourcesPageProps {
- txCostsMedianUsd: MetricReturnData
-}
-
-const EVENT_CATEGORY = "dashboard"
-
-const ResourcesPage = ({ txCostsMedianUsd }: ResourcesPageProps) => {
- const { t } = useTranslation("page-resources")
- const resourceSections = useResources({ txCostsMedianUsd })
- const activeSection = useActiveHash(
- resourceSections.map(({ key }) => key),
- "0% 0% -70% 0%"
- ).replace(/^#/, "")
-
- return (
-
-
- {t("page-resources-banner-notification-message")}{" "}
-
- {t("page-resources-share-feedback")}
-
-
-
-
-
-
-
- {t("page-resources-whats-on-this-page")}
-
-
- {resourceSections.map(({ key, title, icon }) => (
-
- {activeSection === key && (
-
- )}
- {icon && {icon} }
- {title}
-
- ))}
-
-
-
- {resourceSections.map(({ key, icon, title: sectionTitle, boxes }) => (
-
-
-
-
- {icon || }
-
-
- {sectionTitle}
-
-
-
- {boxes.map(({ title, metric, items, className }) => (
-
-
- {title}
-
-
- {metric && metric}
-
- {items.map(({ className, ...item }) => (
-
- ))}
-
-
-
- ))}
-
-
-
- ))}
-
-
-
-
-
-
-
- ethereumdashboards.com
-
-
-
-
-
-
-
-
{t("page-resources-contribute-title")}
-
- {t("page-resources-contribute-description")}
-
-
-
- {/* TODO: Add issue template for resource listing and redirect to new template */}
-
- {t("page-resources-suggest-resource")}
-
-
- {t("page-resources-found-bug")}
-
-
-
-
-
-
-
- )
-}
-
-export default ResourcesPage
diff --git a/app/[locale]/resources/page.tsx b/app/[locale]/resources/page.tsx
index 64827969772..379dadccd19 100644
--- a/app/[locale]/resources/page.tsx
+++ b/app/[locale]/resources/page.tsx
@@ -1,26 +1,35 @@
-import { pick } from "lodash"
-import {
- getMessages,
- getTranslations,
- setRequestLocale,
-} from "next-intl/server"
+import { getTranslations } from "next-intl/server"
+import { FaGithub } from "react-icons/fa6"
import { Lang } from "@/lib/types"
-import I18nProvider from "@/components/I18nProvider"
-
+import BannerNotification from "@/components/Banners/BannerNotification"
+import { HubHero } from "@/components/Hero"
+import StackIcon from "@/components/icons/stack.svg"
+import MainArticle from "@/components/MainArticle"
+import Translation from "@/components/Translation"
+import { ButtonLink } from "@/components/ui/buttons/Button"
+import { Stack, VStack } from "@/components/ui/flex"
+import Link from "@/components/ui/Link"
+import { Section } from "@/components/ui/section"
+
+import { cn } from "@/lib/utils/cn"
import { dataLoader } from "@/lib/utils/data/dataLoader"
import { getMetadata } from "@/lib/utils/metadata"
-import { getRequiredNamespacesForPage } from "@/lib/utils/translations"
+import { GITHUB_REPO_URL } from "@/lib/constants"
import { BASE_TIME_UNIT } from "@/lib/constants"
-import ResourcesPage from "./_components/resources"
+import ResourcesNav from "./_components/ResourcesNav"
+import { ResourceItem, ResourcesContainer } from "./_components/ResourcesUI"
+import { getResources } from "./utils"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"
+import heroImg from "@/public/images/heroes/guides-hub-hero.jpg"
// In seconds
const REVALIDATE_TIME = BASE_TIME_UNIT * 1
+const EVENT_CATEGORY = "dashboard"
const loadData = dataLoader(
[["growThePieData", fetchGrowThePie]],
@@ -30,21 +39,162 @@ const loadData = dataLoader(
const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
const { locale } = await params
- setRequestLocale(locale)
-
- // Get i18n messages
- const allMessages = await getMessages({ locale })
- const requiredNamespaces = getRequiredNamespacesForPage("/resources")
- const messages = pick(allMessages, requiredNamespaces)
+ const t = await getTranslations({ locale, namespace: "page-resources" })
// Load data
const [growThePieData] = await loadData()
const { txCostsMedianUsd } = growThePieData
+ const resourceSections = await getResources({ txCostsMedianUsd })
+
return (
-
-
-
+
+
+ {t("page-resources-banner-notification-message")}{" "}
+
+ {t("page-resources-share-feedback")}
+
+
+
+
+
+
+
+
+ {t("page-resources-whats-on-this-page")}
+
+
+
+
+ {resourceSections.map(({ key, icon, title: sectionTitle, boxes }) => (
+
+
+
+
+ {icon || }
+
+
+ {sectionTitle}
+
+
+
+ {boxes.map(({ title, metric, items, className }) => (
+
+
+ {title}
+
+
+ {metric && metric}
+
+ {items.map(({ className, ...item }) => (
+
+ ))}
+
+
+
+ ))}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ ethereumdashboards.com
+
+
+
+
+
+
+
+
{t("page-resources-contribute-title")}
+
+ {t("page-resources-contribute-description")}
+
+
+
+ {/* TODO: Add issue template for resource listing and redirect to new template */}
+
+ {t("page-resources-suggest-resource")}
+
+
+ {t("page-resources-found-bug")}
+
+
+
+
+
+
+
)
}
diff --git a/src/components/Resources/types.ts b/app/[locale]/resources/types.ts
similarity index 87%
rename from src/components/Resources/types.ts
rename to app/[locale]/resources/types.ts
index 55044e16721..9ed2ccb895d 100644
--- a/src/components/Resources/types.ts
+++ b/app/[locale]/resources/types.ts
@@ -1,4 +1,4 @@
-import { StaticImageData } from "next/image"
+import type { StaticImageData } from "next/image"
export type Item = {
title: string
diff --git a/src/components/Resources/useResources.tsx b/app/[locale]/resources/utils.tsx
similarity index 91%
rename from src/components/Resources/useResources.tsx
rename to app/[locale]/resources/utils.tsx
index 17ae565c83e..1ad61330c9c 100644
--- a/src/components/Resources/useResources.tsx
+++ b/app/[locale]/resources/utils.tsx
@@ -1,5 +1,5 @@
-import { useEffect, useState } from "react"
-import { useLocale } from "next-intl"
+import dynamic from "next/dynamic"
+import { getLocale, getTranslations } from "next-intl/server"
import { Lang } from "@/lib/types"
@@ -8,15 +8,15 @@ import SectionIconEthGlyph from "@/components/icons/eth-glyph.svg"
import SectionIconEthWallet from "@/components/icons/eth-wallet.svg"
import SectionIconHeartPulse from "@/components/icons/heart-pulse.svg"
import SectionIconPrivacy from "@/components/icons/privacy.svg"
+import { Spinner } from "@/components/ui/spinner"
+import { formatSmallUSD } from "@/lib/utils/numbers"
import { getLocaleForNumberFormat } from "@/lib/utils/translations"
-import BigNumber from "../BigNumber"
-import RadialChart from "../RadialChart"
+import BigNumber from "../../../src/components/BigNumber"
import type { DashboardBox, DashboardSection } from "./types"
-import { useTranslation } from "@/hooks/useTranslation"
import IconBeaconchain from "@/public/images/resources/beaconcha-in.png"
import IconBlobsGuru from "@/public/images/resources/blobsguru.png"
import IconBlocknative from "@/public/images/resources/blocknative.png"
@@ -54,19 +54,23 @@ import IconUltrasoundMoney from "@/public/images/resources/ultrasound-money.png"
import IconVisaOnchainAnalytics from "@/public/images/resources/visa-onchain-analytcs.png"
import IconWalletBeat from "@/public/images/resources/walletbeat.png"
-const formatSmallUSD = (value: number, locale: string): string =>
- new Intl.NumberFormat(locale, {
- style: "currency",
- currency: "USD",
- notation: "compact",
- minimumSignificantDigits: 2,
- maximumSignificantDigits: 2,
- }).format(value)
-
-export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
- const { t } = useTranslation("page-resources")
- const locale = useLocale()
- const localeForNumberFormat = getLocaleForNumberFormat(locale! as Lang)
+const SlotCountdownChart = dynamic(
+ () => import("./_components/SlotCountdown"),
+ {
+ ssr: false,
+ loading: () => (
+
+
+
+ ),
+ }
+)
+export const getResources = async ({
+ txCostsMedianUsd,
+}): Promise => {
+ const locale = await getLocale()
+ const t = await getTranslations({ locale, namespace: "page-resources" })
+ const localeForNumberFormat = getLocaleForNumberFormat(locale as Lang)
const medianTxCost =
"error" in txCostsMedianUsd
@@ -76,32 +80,12 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
value: formatSmallUSD(txCostsMedianUsd.value, localeForNumberFormat),
}
- const [timeToNextBlock, setTimeToNextBlock] = useState(12)
-
- useEffect(() => {
- const genesisTime = new Date("2020-12-01T12:00:23Z").getTime()
- const updateTime = () => {
- const now = Date.now()
- const timeElapsed = (now - genesisTime) / 1000
- const timeToNext = 12 - (timeElapsed % 12)
- setTimeToNextBlock(Math.floor(timeToNext) || 12)
- }
-
- updateTime()
- const interval = setInterval(updateTime, 1000)
- return () => clearInterval(interval)
- }, [])
-
const networkBoxes: DashboardBox[] = [
{
title: t("page-resources-network-layer2-title"),
metric: (
-
- Median transaction cost on Ethereum networks
+
+ {t("page-resources-network-layer2-chart-label")}
),
items: [
@@ -130,16 +114,10 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
{
title: t("page-resources-block-explorers-title"),
metric: (
-
+ // CLIENT-SIDE
+
+ {t("page-resources-block-explorers-chart-label")}
+
),
items: [
{
diff --git a/src/components/ActivityStats/index.tsx b/src/components/ActivityStats/index.tsx
index 23b446e0a2c..92b94db6702 100644
--- a/src/components/ActivityStats/index.tsx
+++ b/src/components/ActivityStats/index.tsx
@@ -1,5 +1,3 @@
-import { getLocale } from "next-intl/server"
-
import type { StatsBoxMetric } from "@/lib/types"
import { cn } from "@/lib/utils/cn"
@@ -11,7 +9,6 @@ type ActivityStatsProps = {
className?: string
}
const ActivityStats = async ({ metrics, className }: ActivityStatsProps) => {
- const locale = await getLocale()
const gridBorderClasses = [
"border-b border-body-light xl:border-e xl:pe-8",
"border-b border-body-light xl:ps-8",
@@ -22,7 +19,6 @@ const ActivityStats = async ({ metrics, className }: ActivityStatsProps) => {
{metrics.map(({ label, apiProvider, apiUrl, state }, idx) => (
{
+ const locale = await getLocale()
const t = await getTranslations({ locale, namespace: "common" })
+
const lastUpdatedDisplay =
lastUpdated && isValidDate(lastUpdated)
? new Intl.DateTimeFormat(locale, {
diff --git a/src/intl/en/page-resources.json b/src/intl/en/page-resources.json
index 83878bc0480..f6c3f54e914 100644
--- a/src/intl/en/page-resources.json
+++ b/src/intl/en/page-resources.json
@@ -5,10 +5,12 @@
"page-resources-resilience-title": "Ethereum resilience",
"page-resources-privacy-security-title": "Privacy & security",
"page-resources-network-layer2-title": "Ethereum Networks - Layer 2",
+ "page-resources-network-layer2-chart-label": "Median transaction cost on Ethereum networks",
"page-resources-network-layer2-l2beat-description": "L2BEAT was created to provide transparent and verifiable insights into emerging layer two (L2) technologies which, in line with the rollup-centric Ethereum scaling roadmap, are aimed at scaling Ethereum.",
"page-resources-network-layer2-growthepie-description": "Mastering Ethereum Layer 2s. Your gateway to curated analytics and knowledge.",
"page-resources-network-layer2-l2fees-description": "How much does it cost to use Layer 2?",
"page-resources-block-explorers-title": "Block explorers",
+ "page-resources-block-explorers-chart-label": "Time to next block",
"page-resources-block-explorers-blockscout-description": "Open-source block explorer by Blockscout with complete blockchain data and APIs for Ethereum networks.",
"page-resources-block-explorers-etherscan-description": "Etherscan is a block explorer and analytics platform for Ethereum, a decentralized smart contracts platform.",
"page-resources-block-explorers-beaconchain-description": "Open source Ethereum explorer showing the Ethereum Mainnet 🚀.",