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 ( + + ) +} + +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, 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 🚀.",