+)
+
+export { Skeleton, SkeletonLines }
diff --git a/src/data/community-events.json b/src/data/community-events.json
index 8b1045c4742..98bbe0249f3 100644
--- a/src/data/community-events.json
+++ b/src/data/community-events.json
@@ -520,7 +520,7 @@
"href": "https://ethmumbai.in",
"location": "Mumbai, IND",
"description": "First ever Ethereum Hackathon in Mumbai. Build from Mumbai, for the world. 29-31 March 2024.",
- "imageUrl": ""
+ "imageUrl": "https://ethmumbai.in/post.png"
},
{
"title": "ETH Gathering",
@@ -565,7 +565,7 @@
"href": "https://ethwarsaw.dev",
"location": "Warsaw, POL",
"description": "EthWarsaw 2024",
- "imageUrl": ""
+ "imageUrl": "https://cdn.prod.website-files.com/649014d99c5194ad73558cd3/649af7b1f1fdaa7868890166_ThumbnailPicture.png"
},
{
"title": "NapulETH",
diff --git a/src/data/placeholders/content-contributing-translation-program-translatathon-translatathon-hubs-data.json b/src/data/placeholders/content-contributing-translation-program-translatathon-translatathon-hubs-data.json
new file mode 100644
index 00000000000..34cfd9c5b0b
--- /dev/null
+++ b/src/data/placeholders/content-contributing-translation-program-translatathon-translatathon-hubs-data.json
@@ -0,0 +1,6 @@
+{
+ "/content/contributing/translation-program/translatathon/translatathon-hubs/local-communities.png": {
+ "hash": "d0fbc65b",
+ "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAGCAYAAADKfB7nAAAACXBIWXMAABYlAAAWJQFJUiTwAAABkUlEQVR4nAGGAXn+AAAANPsHFkf8Fg8y/EcdOvy+jpT88cjC/K+QmPyljqH848TS/P/ZyPx/cYP8R0p+/AAKR/xjVn/8b0li/BwMOPoAAAA4/xIgU/8YFTv/gEBV/3xum/9dhNb/ZWOf/3+Pv/+di7//e3bI/1Oz1/8mSX//AAA//0w5af84Hkn/IhA6/wAABDz/Dx9S/0EsTP+GSFz/p2hy/7fF2f+xttH/5Nfd/8uqw/+0tOT/zYmI/4J4n/+Yg47/t2Vd/zwpVv86JE3/AAAPSv8LI2D/FhtV/w0VUf9bQGv/n36k/5aKq//2+v//0fr//2likv/Nj6f/04Wh/3lacv9SKUj/AhFP/ykjWf8AAR5b/xMoaf80SI//M0eS/4NtpP+jb6H/l6fO/73e//++2f//YGnc/5h/qv+tYWb/o2hw/4uCp/9MSX7/ABhf/wANLGn5ARZS+ztEgPpPV5L8R1ef+1NeqPqGeqz6cn/F+k1fufpPOoD8f1Fx/8J4cf/7mW7/04t4+1VFcPsAFVb0G7fcN+dEqnEAAAAASUVORK5CYII="
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/useRtlFlip.ts b/src/hooks/useRtlFlip.ts
index b3a39369818..3c684d06800 100644
--- a/src/hooks/useRtlFlip.ts
+++ b/src/hooks/useRtlFlip.ts
@@ -7,7 +7,7 @@ import { isLangRightToLeft } from "@/lib/utils/translations"
type UseDirection = {
/** @deprecated */
flipForRtl: "scaleX(-1)" | undefined // transform (deprecated)
- twFlipForRtl: "-scale-x-1" | "" // className
+ twFlipForRtl: "-scale-x-100" | "" // className
isRtl: boolean
direction: "ltr" | "rtl"
}
@@ -22,7 +22,7 @@ export const useRtlFlip = (): UseDirection => {
const isRtl = isLangRightToLeft(locale as Lang)
return {
flipForRtl: isRtl ? "scaleX(-1)" : undefined, // transform (deprecated)
- twFlipForRtl: isRtl ? "-scale-x-1" : "", // className (preferred)
+ twFlipForRtl: isRtl ? "-scale-x-100" : "", // className (preferred)
isRtl,
direction: isRtl ? "rtl" : "ltr",
}
diff --git a/src/intl/en/common.json b/src/intl/en/common.json
index 4eb2fc148cd..134b022c7b7 100644
--- a/src/intl/en/common.json
+++ b/src/intl/en/common.json
@@ -198,6 +198,7 @@
"languages": "Languages",
"last-24-hrs": "Last 24 hours",
"last-edit": "Last edit",
+ "last-updated": "Last updated",
"layer-2": "Layer 2",
"learn": "Learn",
"learn-by-coding": "Learn by coding",
@@ -431,4 +432,4 @@
"wrapped-ether": "Wrapped Ether",
"yes": "Yes",
"zero-knowledge-proofs": "Zero-knowledge proofs"
-}
+}
\ No newline at end of file
diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json
index 9a4befae773..d4c87175069 100644
--- a/src/intl/en/page-index.json
+++ b/src/intl/en/page-index.json
@@ -1,10 +1,23 @@
{
- "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.",
- "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.",
- "page-index-meta-title": "Home",
- "page-index-title": "Welcome to Ethereum",
- "page-index-description": "Ethereum is the community-run technology powering the cryptocurrency ether (ETH) and thousands of decentralized applications.",
"page-index-title-button": "Explore Ethereum",
+ "page-index-touts-header": "Explore ethereum.org",
+ "page-index-contribution-banner-title": "Contribute to ethereum.org",
+ "page-index-contribution-banner-description": "This website is open source with hundreds of community contributors. You can propose edits to any of the content on this site, suggest awesome new features, or help us squash bugs.",
+ "page-index-contribution-banner-image-alt": "An Ethereum logo made of lego bricks.",
+ "page-index-contribution-banner-button": "More on contributing",
+ "page-index-tout-upgrades-title": "Level up your upgrade knowledge",
+ "page-index-tout-upgrades-description": "The Ethereum roadmap consists of interconnected upgrades designed to make the network more scalable, secure, and sustainable.",
+ "page-index-tout-upgrades-image-alt": "Illustration of a spaceship representing the increased power after Ethereum upgrades.",
+ "page-index-tout-enterprise-title": "Ethereum for enterprise",
+ "page-index-tout-enterprise-description": "See how Ethereum can open up new business models, reduce your costs and future-proof your business.",
+ "page-index-tout-enterprise-image-alt": "Illustration of a futuristic computer/device.",
+ "page-index-tout-community-title": "The Ethereum community",
+ "page-index-tout-community-description": "Ethereum is all about community. It's made up of people from all different backgrounds and interests. See how you can join in.",
+ "page-index-tout-community-image-alt": "Illustration of a group of builders working together.",
+ "page-index-nft": "The internet of assets",
+ "page-index-nft-description": "Ethereum isn't just for digital money. Anything you can own can be represented, traded and put to use as non-fungible tokens (NFTs). You can tokenise your art and get royalties automatically every time it's re-sold. Or use a token for something you own to take out a loan. The possibilities are growing all the time.",
+ "page-index-nft-button": "More on NFTs",
+ "page-index-nft-alt": "An Eth logo being displayed via hologram.",
"page-index-get-started": "Get started",
"page-index-get-started-description": "ethereum.org is your portal into the world of Ethereum. The tech is new and ever-evolving – it helps to have a guide. Here's what we recommend you do if you want to dive in.",
"page-index-get-started-image-alt": "Illustration of a person working on a computer.",
@@ -37,45 +50,8 @@
"page-index-developers": "A new frontier for development",
"page-index-developers-description": "Ethereum and its apps are transparent and open source. You can fork code and re-use functionality others have already built. If you don't want to learn a new language you can just interact with open-sourced code using JavaScript and other existing languages.",
"page-index-developers-button": "Developer portal",
- "page-index-developers-code-examples": "Code examples",
- "page-index-developers-code-example-title-0": "Your own bank",
- "page-index-developers-code-example-description-0": "You can build a bank powered by logic you've programmed.",
- "page-index-developers-code-example-title-1": "Your own currency",
- "page-index-developers-code-example-description-1": "You can create tokens that you can transfer and use across applications.",
- "page-index-developers-code-example-title-2": "A JavaScript Ethereum wallet",
- "page-index-developers-code-example-description-2": "You can use existing languages to interact with Ethereum and other applications.",
- "page-index-developers-code-example-title-3": "An open, permissionless DNS",
- "page-index-developers-code-example-description-3": "You can reimagine existing services as decentralized, open applications.",
- "page-index-network-stats-title": "Ethereum today",
- "page-index-network-stats-subtitle": "The latest network statistics",
- "page-index-network-stats-total-eth-staked": "Total ETH staked",
- "page-index-network-stats-eth-price-description": "ETH price (USD)",
- "page-index-network-stats-eth-price-explainer": "The latest price for 1 ether. You can buy as little as 0.000000000000000001 – you don't need to buy 1 whole ETH.",
- "page-index-network-stats-total-eth-staked-explainer": "The total amount of ETH currently being staked and securing the network.",
- "page-index-network-stats-tx-day-description": "Transactions today",
- "page-index-network-stats-tx-day-explainer": "The number of transactions successfully processed on the network in the last 24 hours.",
- "page-index-network-stats-value-defi-description": "Value locked in DeFi (USD)",
- "page-index-network-stats-value-defi-explainer": "The amount of money in decentralized finance (DeFi) applications, the Ethereum digital economy.",
"page-index-network-stats-nodes-description": "Nodes",
"page-index-network-stats-nodes-explainer": "Ethereum is run by thousands of volunteers around the globe, known as nodes.",
- "page-index-touts-header": "Explore ethereum.org",
- "page-index-contribution-banner-title": "Contribute to ethereum.org",
- "page-index-contribution-banner-description": "This website is open source with hundreds of community contributors. You can propose edits to any of the content on this site, suggest awesome new features, or help us squash bugs.",
- "page-index-contribution-banner-image-alt": "An Ethereum logo made of lego bricks.",
- "page-index-contribution-banner-button": "More on contributing",
- "page-index-tout-upgrades-title": "Level up your upgrade knowledge",
- "page-index-tout-upgrades-description": "The Ethereum roadmap consists of interconnected upgrades designed to make the network more scalable, secure, and sustainable.",
- "page-index-tout-upgrades-image-alt": "Illustration of a spaceship representing the increased power after Ethereum upgrades.",
- "page-index-tout-enterprise-title": "Ethereum for enterprise",
- "page-index-tout-enterprise-description": "See how Ethereum can open up new business models, reduce your costs and future-proof your business.",
- "page-index-tout-enterprise-image-alt": "Illustration of a futuristic computer/device.",
- "page-index-tout-community-title": "The Ethereum community",
- "page-index-tout-community-description": "Ethereum is all about community. It's made up of people from all different backgrounds and interests. See how you can join in.",
- "page-index-tout-community-image-alt": "Illustration of a group of builders working together.",
- "page-index-nft": "The internet of assets",
- "page-index-nft-description": "Ethereum isn't just for digital money. Anything you can own can be represented, traded and put to use as non-fungible tokens (NFTs). You can tokenise your art and get royalties automatically every time it's re-sold. Or use a token for something you own to take out a loan. The possibilities are growing all the time.",
- "page-index-nft-button": "More on NFTs",
- "page-index-nft-alt": "An Eth logo being displayed via hologram.",
"community-events-content-heading": "Join the ethereum.org community",
"community-events-content-1": "Join almost 40 000 members on our Discord server.",
"community-events-content-2": "Join our monthly community calls for exciting updates on Ethereum.org development and important ecosystem news. Get the chance to ask questions, share ideas, and provide feedback - it's the perfect opportunity to be part of the thriving Ethereum community.",
@@ -85,5 +61,92 @@
"community-events-no-upcoming-calls": "No upcoming calls",
"community-events-previous-calls": "Previous calls",
"community-events-there-are-no-past-calls": "There are no past calls",
- "community-events-add-to-calendar": "Add to calendar"
+ "community-events-add-to-calendar": "Add to calendar",
+ "page-index-activity-description": "Activity from all Ethereum networks",
+ "page-index-activity-tag": "Activity",
+ "page-index-activity-header": "The strongest ecosystem",
+ "page-index-bento-header": "A new way to use the internet",
+ "page-index-bento-assets-action": "More on NFTs",
+ "page-index-bento-assets-content": "Art, certificates or even real estate can be tokenized. Anything can be a tradable token. Ownership is public and verifiable.",
+ "page-index-bento-assets-title": "The internet of assets",
+ "page-index-bento-dapps-action": "Browse apps",
+ "page-index-bento-dapps-content": "Ethereum apps work without selling your data. Protect your privacy.",
+ "page-index-bento-dapps-title": "Innovative apps",
+ "page-index-bento-defi-action": "Explore DeFi",
+ "page-index-bento-defi-content": "Billions can't open bank accounts or freely use their money. Ethereum's financial system is always open and unbiased.",
+ "page-index-bento-defi-title": "A fairer financial system",
+ "page-index-bento-networks-action": "Explore benefits",
+ "page-index-bento-networks-content": "Ethereum is the hub for blockchain innovation. The best project are built on Ethereum.",
+ "page-index-bento-networks-title": "The network of networks",
+ "page-index-bento-stablecoins-action": "Learn more",
+ "page-index-bento-stablecoins-content": "Stablecoins are currencies that maintain stable value. Their price matches the U.S. dollar or other steady asset.",
+ "page-index-bento-stablecoins-title": "Crypto without volatility",
+ "page-index-builders-action-primary": "Builder's Portal",
+ "page-index-builders-action-secondary": "Documentation",
+ "page-index-builders-description": "Ethereum is home to Web3's largest and most vibrant developer ecosystem. Use JavaScript and Python, or learn a smart contract language like Solidity or Vyper to write your own app.",
+ "page-index-builders-tag": "Builders",
+ "page-index-builders-header": "Blockchain's biggest builder community",
+ "page-index-calendar-add": "Add to calendar",
+ "page-index-calendar-fallback": "No upcoming calls",
+ "page-index-calendar-title": "Next calls",
+ "page-index-community-action": "More on ethereum.org",
+ "page-index-community-description-1": "The ethereum.org website is built and maintained by hundreds of translators, coders, designers, copywriters, and enthusiastic community members each month.",
+ "page-index-community-description-2": "Come ask questions, connect with people around the world and contribute to the website. You will get relevant practical experience and be guided during the process!",
+ "page-index-community-description-3": "Ethereum.org community is the perfect place to start and learn.",
+ "page-index-community-tag": "Ethereum.org Community",
+ "page-index-community-header": "Built by the community",
+ "page-index-cta-dapps-description": "See what Ethereum can do",
+ "page-index-cta-dapps-label": "Try apps",
+ "page-index-cta-get-eth-description": "The currency of Ethereum",
+ "page-index-cta-get-eth-label": "Get ETH",
+ "page-index-cta-networks-description": "Enjoy minimal fees",
+ "page-index-cta-networks-label": "Choose a network",
+ "page-index-cta-wallet-description": "Create accounts, manage assets",
+ "page-index-cta-wallet-label": "Pick a wallet",
+ "page-index-description": "The leading platform for innovative apps and Ethereum-backed blockchain networks",
+ "page-index-developers-code-example-description-0": "Build a bank powered by logic you've programmed",
+ "page-index-developers-code-example-description-1": "Create tokens that you can transfer and use across applications",
+ "page-index-developers-code-example-description-2": "Use existing languages to interact with Ethereum and other applications",
+ "page-index-developers-code-example-description-3": "Reimagine existing services as decentralized, open applications",
+ "page-index-developers-code-example-title-0": "Your own bank",
+ "page-index-developers-code-example-title-1": "Your own currency",
+ "page-index-developers-code-example-title-2": "A JavaScript Ethereum wallet",
+ "page-index-developers-code-example-title-3": "An open, permissionless DNS",
+ "page-index-developers-code-examples": "Code examples",
+ "page-index-events-action": "See all events",
+ "page-index-events-header": "Events",
+ "page-index-events-subtitle": "Ethereum communities host events all around the globe, all year long",
+ "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.",
+ "page-index-join-action-contribute-description": "Find out all the different ways you can help ethereum.org grow and be better.",
+ "page-index-join-action-contribute-label": "How to contribute",
+ "page-index-join-action-discord-description": "To ask questions, coordinate contribution and join community calls.",
+ "page-index-join-action-github-description": "Contribute to code, content, articles, etc.",
+ "page-index-join-action-twitter-description": "To keep up with our updates and important news.",
+ "page-index-join-description": "This website is open source with hundreds of community contributors. You can propose edits to any of the content on this site.",
+ "page-index-join-header": "Join ethereum.org",
+ "page-index-learn-description": "Crypto can feel overwhelming. Don't worry, these materials are designed to help you understand Ethereum in just a few minutes.",
+ "page-index-learn-tag": "Learn",
+ "page-index-learn-header": "Understand Ethereum",
+ "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.",
+ "page-index-meta-title": "Home",
+ "page-index-network-stats-subtitle": "The latest network statistics",
+ "page-index-network-stats-title": "Ethereum today",
+ "page-index-network-stats-total-eth-staked-explainer": "The total amount of ETH currently being staked and securing the network.",
+ "page-index-network-stats-total-eth-staked": "Value protecting Ethereum",
+ "page-index-network-stats-tx-cost-description": "Average transaction cost",
+ "page-index-network-stats-tx-day-description": "Transactions in the last 24h",
+ "page-index-network-stats-tx-day-explainer": "The number of transactions successfully processed on the network in the last 24 hours.",
+ "page-index-network-stats-value-defi-description": "Value locked in DeFi (USD)",
+ "page-index-network-stats-value-defi-explainer": "The amount of money in decentralized finance (DeFi) applications, the Ethereum digital economy.",
+ "page-index-popular-topics-ethereum": "What is Ethereum?",
+ "page-index-popular-topics-header": "Popular topics",
+ "page-index-popular-topics-action": "Other topics",
+ "page-index-popular-topics-roadmap": "Ethereum roadmap",
+ "page-index-popular-topics-start": "How to start, step by step",
+ "page-index-popular-topics-wallets": "What are crypto wallets?",
+ "page-index-popular-topics-whitepaper": "Ethereum Whitepaper",
+ "page-index-posts-action": "Read more on these websites",
+ "page-index-posts-header": "Recent posts",
+ "page-index-posts-subtitle": "The latest blog posts and updates from the community",
+ "page-index-title": "Welcome to Ethereum"
}
diff --git a/src/lib/api/fetchEthPrice.ts b/src/lib/api/fetchEthPrice.ts
new file mode 100644
index 00000000000..a86f6e1d8cc
--- /dev/null
+++ b/src/lib/api/fetchEthPrice.ts
@@ -0,0 +1,17 @@
+import { MetricReturnData } from "../types"
+
+export const fetchEthPrice = async (): Promise => {
+ try {
+ const data: { ethereum: { usd: number } } = await fetch(
+ "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd"
+ ).then((res) => res.json())
+ const {
+ ethereum: { usd },
+ } = data
+ if (!usd) throw new Error("Unable to fetch ETH price from CoinGecko")
+ return { value: usd, timestamp: Date.now() }
+ } catch (error: unknown) {
+ console.error((error as Error).message)
+ return { error: (error as Error).message }
+ }
+}
diff --git a/src/lib/api/fetchGrowThePie.ts b/src/lib/api/fetchGrowThePie.ts
new file mode 100644
index 00000000000..1affc794c3b
--- /dev/null
+++ b/src/lib/api/fetchGrowThePie.ts
@@ -0,0 +1,67 @@
+import type { GrowThePieData } from "../types"
+
+type DataItem = {
+ metric_key: string
+ origin_key: string
+ date: string
+ value: number
+}
+
+const TXCOSTS_MEDIAN_USD = "txcosts_median_usd"
+const TXCOUNT = "txcount"
+
+export const fetchGrowThePie = async (): Promise => {
+ const url = "https://api.growthepie.xyz/v1/fundamentals_full.json"
+ try {
+ const response = await fetch(url)
+ if (!response.ok) {
+ console.log(response.status, response.statusText)
+ throw new Error("Failed to fetch GrowThePie data")
+ }
+ const data: DataItem[] = await response.json()
+
+ const mostRecentDate = data.reduce((latest, item) => {
+ const itemDate = new Date(item.date)
+ return itemDate > new Date(latest) ? item.date : latest
+ }, data[0].date)
+
+ const mostRecentData = data.filter(
+ (item) =>
+ item.date === mostRecentDate &&
+ [TXCOSTS_MEDIAN_USD, TXCOUNT].includes(item.metric_key)
+ )
+
+ let totalTxCount = 0
+ let weightedSum = 0
+
+ mostRecentData.forEach((item) => {
+ if (item.metric_key !== TXCOSTS_MEDIAN_USD) return
+
+ const txCountItem = mostRecentData.find(
+ (txItem) =>
+ txItem.metric_key === TXCOUNT && txItem.origin_key === item.origin_key
+ )
+ if (!txCountItem) return
+
+ totalTxCount += txCountItem.value
+ weightedSum += item.value * txCountItem.value
+ })
+
+ // The weighted average of txcosts_median_usd, by txcount on each network (origin_key)
+ const weightedAverage = totalTxCount ? weightedSum / totalTxCount : 0
+
+ // Last updated timestamp
+ const timestamp = Date.now()
+
+ return {
+ txCount: { value: totalTxCount, timestamp },
+ txCostsMedianUsd: { value: weightedAverage, timestamp },
+ }
+ } catch (error: unknown) {
+ console.error((error as Error).message)
+ return {
+ txCount: { error: (error as Error).message },
+ txCostsMedianUsd: { error: (error as Error).message },
+ }
+ }
+}
diff --git a/src/lib/api/fetchNodes.ts b/src/lib/api/fetchNodes.ts
deleted file mode 100644
index 37f22e7f9ec..00000000000
--- a/src/lib/api/fetchNodes.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type {
- EtherscanNodeResponse,
- MetricReturnData,
- TimestampedData,
-} from "@/lib/types"
-
-import { DAYS_TO_FETCH, ETHERSCAN_API_URL } from "@/lib/constants"
-
-export const fetchNodes = async (): Promise => {
- const apiKey = process.env.ETHERSCAN_API_KEY
- const now = new Date()
- const endDate = now.toISOString().split("T")[0] // YYYY-MM-DD
- const startDate = new Date(now.setDate(now.getDate() - DAYS_TO_FETCH))
- .toISOString()
- .split("T")[0] // {daysToFetch} days ago
-
- const queryParams = new URLSearchParams({
- module: "stats",
- action: "nodecounthistory",
- startdate: startDate,
- enddate: endDate,
- sort: "desc",
- apikey: apiKey,
- }).toString()
-
- const { href } = new URL(`api?${queryParams}`, ETHERSCAN_API_URL)
-
- try {
- const response = await fetch(href)
- if (!response.ok) {
- console.log(response.status, response.statusText)
- throw new Error("Failed to fetch Etherscan node data")
- }
-
- const json: EtherscanNodeResponse = await response.json()
- const data: TimestampedData[] = json.result
- .map(({ UTCDate, TotalNodeCount }) => ({
- timestamp: new Date(UTCDate).getTime(),
- value: +TotalNodeCount,
- }))
- .sort((a, b) => a.timestamp - b.timestamp)
- const { value } = data[data.length - 1]
-
- return {
- data, // historical data: { timestamp: unix-milliseconds, value }
- value, // current value (number, unformatted)
- }
- } catch (error: unknown) {
- console.error((error as Error).message)
- return { error: (error as Error).message }
- }
-}
diff --git a/src/lib/api/fetchPosts.ts b/src/lib/api/fetchPosts.ts
new file mode 100644
index 00000000000..31b1bea95a6
--- /dev/null
+++ b/src/lib/api/fetchPosts.ts
@@ -0,0 +1,39 @@
+import type { HTMLResult, RSSItem } from "../types"
+
+import { fetchXml } from "./fetchRSS"
+
+export const fetchAttestantPosts = async () => {
+ const BASE_URL = "https://www.attestant.io/posts/"
+ const htmlData = (await fetchXml(BASE_URL)) as HTMLResult
+
+ // Extract div containing list of posts from deeply nested HTML structure
+ const postsContainer =
+ htmlData.html.body[0].div[0].div[1].div[0].div[0].div[0].div
+
+ const posts: RSSItem[] = postsContainer
+ .map(({ a }) => {
+ const [
+ {
+ $: { href },
+ h4: [{ _: title }],
+ div: [{ _: content }, { _: pubDate }],
+ },
+ ] = a
+ const { href: link } = new URL(href, BASE_URL)
+ return {
+ title,
+ link,
+ content,
+ source: "Attestant",
+ sourceUrl: BASE_URL,
+ sourceFeedUrl: BASE_URL,
+ imgSrc: "/images/attestant-logo.svg",
+ pubDate,
+ }
+ })
+ .sort(
+ (a: RSSItem, b: RSSItem) =>
+ new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime()
+ )
+ return posts
+}
diff --git a/src/lib/api/fetchRSS.ts b/src/lib/api/fetchRSS.ts
new file mode 100644
index 00000000000..f191e3091bf
--- /dev/null
+++ b/src/lib/api/fetchRSS.ts
@@ -0,0 +1,138 @@
+import { parseString } from "xml2js"
+
+import type {
+ AtomElement,
+ AtomResult,
+ RSSChannel,
+ RSSItem,
+ RSSResult,
+} from "../types"
+import { isValidDate } from "../utils/date"
+
+/**
+ * Fetches RSS feed from the specified XML URL(s).
+ * @param xmlUrl - The URL(s) of the XML feed to fetch.
+ * @returns An array sources, each containing an array of RSS items
+ */
+export const fetchRSS = async (xmlUrl: string | string[]) => {
+ const urls = Array.isArray(xmlUrl) ? xmlUrl : [xmlUrl]
+ const allItems: RSSItem[][] = []
+ for (const url of urls) {
+ const response = (await fetchXml(url)) as RSSResult | AtomResult
+ if ("rss" in response) {
+ const [mainChannel] = response.rss.channel as RSSChannel[]
+ const [source] = mainChannel.title
+ const [sourceUrl] = mainChannel.link
+ const channelImage = mainChannel.image ? mainChannel.image[0].url[0] : ""
+
+ const parsedRssItems = mainChannel.item
+ // Filter out items with invalid dates
+ .filter((item) => {
+ if (!item.pubDate) return false
+ const [pubDate] = item.pubDate
+ return isValidDate(pubDate)
+ })
+ // Sort by pubDate (most recent is first in array
+ .sort((a, b) => {
+ const dateA = new Date(a.pubDate[0])
+ const dateB = new Date(b.pubDate[0])
+ return dateB.getTime() - dateA.getTime()
+ })
+ // Map to RSSItem object
+ .map((item) => {
+ const getImgSrc = () => {
+ if (item.enclosure) return item.enclosure[0].$.url
+ if (item["media:content"]) return item["media:content"][0].$.url
+ return channelImage
+ }
+ return {
+ pubDate: item.pubDate[0],
+ title: item.title[0],
+ link: item.link[0],
+ imgSrc: getImgSrc(),
+ source,
+ sourceUrl,
+ sourceFeedUrl: url,
+ } as RSSItem
+ })
+
+ allItems.push(parsedRssItems)
+ } else if ("feed" in response) {
+ const [source] = response.feed.title
+ const [sourceUrl] = response.feed.id
+ const feedImage = response.feed.icon?.[0]
+
+ const parsedAtomItems = response.feed.entry
+ // Filter out items with invalid dates
+ .filter((entry) => {
+ if (!entry.updated) return false
+ const [published] = entry.updated
+ return isValidDate(published)
+ })
+ // Sort by published (most recent is first in array
+ .sort((a, b) => {
+ const dateA = new Date(a.updated[0])
+ const dateB = new Date(b.updated[0])
+ return dateB.getTime() - dateA.getTime()
+ })
+ // Map to RSSItem object
+ .map((entry) => {
+ const getString = (el?: AtomElement[]): string => {
+ if (!el) return ""
+ const [firstEl] = el
+ if (typeof firstEl === "string") return firstEl
+ return firstEl._ || ""
+ }
+ const getHref = (): string => {
+ if (!entry.link) {
+ console.warn(`No link found for RSS url: ${url}`)
+ return ""
+ }
+ const link = entry.link[0]
+ if (typeof link === "string") return link
+ return link.$.href || ""
+ }
+ const getImgSrc = (): string => {
+ const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g
+ const contentMatch = getString(entry.content).match(imgRegEx)
+ if (contentMatch) return contentMatch[0]
+ const summaryMatch = getString(entry.summary).match(imgRegEx)
+ if (summaryMatch) return summaryMatch[0]
+ return feedImage || ""
+ }
+ return {
+ pubDate: entry.updated[0],
+ title: getString(entry.title),
+ link: getHref(),
+ imgSrc: getImgSrc(),
+ source,
+ sourceUrl,
+ sourceFeedUrl: url,
+ } as RSSItem
+ })
+
+ allItems.push(parsedAtomItems)
+ }
+ }
+ return allItems as RSSItem[][]
+}
+
+/**
+ * Fetches XML data from the specified URL.
+ * Parses XML to JSON with parseString (xml2js package)
+ * @param url - The URL to fetch the XML data from.
+ * @returns A promise that resolves to the parsed XML data as a JSON object.
+ */
+export const fetchXml = async (url: string) => {
+ const response = await fetch(url)
+ const xml = await response.text()
+ let returnObject: Record = {}
+ parseString(xml, (err, result) => {
+ if (err) {
+ console.error(err)
+ return
+ }
+ returnObject = result
+ })
+ return returnObject
+}
diff --git a/src/lib/api/fetchTotalEthStaked.ts b/src/lib/api/fetchTotalEthStaked.ts
index 87953e83d15..7828977b5d1 100644
--- a/src/lib/api/fetchTotalEthStaked.ts
+++ b/src/lib/api/fetchTotalEthStaked.ts
@@ -13,33 +13,23 @@ export const fetchTotalEthStaked = async (): Promise => {
const url = new URL("api/v1/query/3915587/results", DUNE_API_URL)
try {
- const ethStakedResponse = await fetch(url, {
+ const response = await fetch(url, {
headers: { "X-Dune-API-Key": DUNE_API_KEY },
})
- if (!ethStakedResponse.ok) {
- console.log(ethStakedResponse.status, ethStakedResponse.statusText)
+ if (!response.ok) {
+ console.log(response.status, response.statusText)
throw new Error("Failed to fetch eth staked data")
}
- const ethStakedJson: EthStakedResponse = await ethStakedResponse.json()
+ const json: EthStakedResponse = await response.json()
const {
result: { rows = [] },
- } = ethStakedJson
+ } = json
+ // Today's value at start of array
+ const value = rows[0].cum_deposited_eth
- const data = rows.map((row) => ({
- timestamp: new Date(row.time).getTime(),
- value: row.cum_deposited_eth,
- }))
-
- // data is already sorted...but just in case
- data.sort((a, b) => a.timestamp - b.timestamp)
-
- const { value } = data[data.length - 1]
-
- return {
- data,
- value,
- }
+ // current value (number, unformatted)
+ return { value, timestamp: Date.now() }
} catch (error: unknown) {
console.error((error as Error).message)
return { error: (error as Error).message }
diff --git a/src/lib/api/fetchTotalValueLocked.ts b/src/lib/api/fetchTotalValueLocked.ts
index 5299b86792b..cf9cbb1a256 100644
--- a/src/lib/api/fetchTotalValueLocked.ts
+++ b/src/lib/api/fetchTotalValueLocked.ts
@@ -1,14 +1,6 @@
-import takeRightWhile from "lodash/takeRightWhile"
-
import { DefiLlamaTVLResponse, MetricReturnData } from "@/lib/types"
-import { DAYS_TO_FETCH } from "@/lib/constants"
-
export const fetchTotalValueLocked = async (): Promise => {
- const now = new Date()
- const startDate = new Date(now.setDate(now.getDate() - DAYS_TO_FETCH))
- const startTimestamp = Math.round(startDate.getTime() / 1000)
-
try {
const response = await fetch(`https://api.llama.fi/charts/Ethereum`)
if (!response.ok) {
@@ -17,18 +9,11 @@ export const fetchTotalValueLocked = async (): Promise => {
}
const json: DefiLlamaTVLResponse = await response.json()
- const data = takeRightWhile(json, ({ date }) => +date > startTimestamp)
- .map(({ date, totalLiquidityUSD }) => ({
- timestamp: +date * 1000,
- value: totalLiquidityUSD,
- }))
- .sort((a, b) => a.timestamp - b.timestamp)
- const { value } = data[data.length - 1]
+ // Today's value at end of array
+ const value = json[json.length - 1].totalLiquidityUSD
- return {
- data, // historical data: { timestamp: unix-milliseconds, value }
- value, // current value (number, unformatted)
- }
+ // current value (number, unformatted)
+ return { value, timestamp: Date.now() }
} catch (error: unknown) {
console.error((error as Error).message)
return { error: (error as Error).message }
diff --git a/src/lib/api/fetchTxCount.ts b/src/lib/api/fetchTxCount.ts
deleted file mode 100644
index a68c2f633df..00000000000
--- a/src/lib/api/fetchTxCount.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type {
- EtherscanTxCountResponse,
- MetricReturnData,
- TimestampedData,
-} from "@/lib/types"
-
-import { DAYS_TO_FETCH, ETHERSCAN_API_URL } from "@/lib/constants"
-
-export const fetchTxCount = async (): Promise => {
- const apiKey = process.env.ETHERSCAN_API_KEY
- const now = new Date()
- const endDate = now.toISOString().split("T")[0] // YYYY-MM-DD
- const startDate = new Date(now.setDate(now.getDate() - DAYS_TO_FETCH))
- .toISOString()
- .split("T")[0] // {DAYS_TO_FETCH} days ago
-
- const queryParams = new URLSearchParams({
- module: "stats",
- action: "dailytx",
- startdate: startDate,
- enddate: endDate,
- sort: "asc",
- apikey: apiKey,
- }).toString()
-
- const { href } = new URL(`api?${queryParams}`, ETHERSCAN_API_URL)
-
- try {
- const response = await fetch(href)
- if (!response.ok) {
- console.log(response.status, response.statusText)
- throw new Error("Failed to fetch Etherscan tx count data")
- }
-
- const json: EtherscanTxCountResponse = await response.json()
- const data: TimestampedData[] = json.result
- .map(({ unixTimeStamp, transactionCount }) => ({
- timestamp: +unixTimeStamp * 1000, // unix milliseconds
- value: transactionCount,
- }))
- .sort((a, b) => a.timestamp - b.timestamp)
- const { value } = data[data.length - 1]
-
- return {
- data, // historical data: { timestamp: unix-milliseconds, value }
- value, // current value (number, unformatted)
- }
- } catch (error: unknown) {
- console.error((error as Error).message)
- return {
- error: (error as Error).message,
- }
- }
-}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 63fd3212299..df5f68b4176 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -4,6 +4,8 @@ import { NavSectionKey } from "@/components/Nav/types"
import i18nConfig from "../../i18n.config.json"
+import type { CommunityBlog } from "./types"
+
export const OLD_CONTENT_DIR = "src/content"
export const CONTENT_DIR = "public/content"
export const TRANSLATIONS_DIR = "public/content/translations"
@@ -25,6 +27,8 @@ export const LOCALES_CODES = BUILD_LOCALES
// Site urls
export const SITE_URL = "https://ethereum.org"
export const DISCORD_PATH = "/discord/"
+export const GITHUB_REPO_URL =
+ "https://github.com/ethereum/ethereum-org-website"
export const EDIT_CONTENT_URL = `https://github.com/ethereum/ethereum-org-website/tree/dev/`
export const MAIN_CONTENT_ID = "main-content"
export const WEBSITE_EMAIL = "website@ethereum.org"
@@ -71,8 +75,7 @@ export const REGULAR_RATES: ReportsModel.RegularRate[] = [
export const languagePathRootRegExp = /^.+\/content\/translations\/[a-z-]*\//
// Metrics
-export const DAYS_TO_FETCH = 90
-export const RANGES = ["30d", "90d"] as const
+export const DAYS_TO_FETCH = 1
export const BEACONCHA_IN_URL = "https://beaconcha.in/"
export const ETHERSCAN_API_URL = "https://api.etherscan.io"
export const DUNE_API_URL = "https://api.dune.com"
@@ -156,3 +159,59 @@ export const DESKTOP_LANGUAGE_BUTTON_NAME = "desktop-language-button"
// Codeblock
export const LINES_BEFORE_COLLAPSABLE = 8
+
+// Ethereum.org community
+export const CALENDAR_DISPLAY_COUNT = 4
+
+// RSS Feeds
+export const RSS_DISPLAY_COUNT = 6
+
+export const VITALIK_FEED = "https://vitalik.eth.limo/feed.xml"
+export const SOLIDITY_FEED = "https://soliditylang.org/feed.xml"
+export const _0X_PARC_FEED = "https://rss.app/feeds/cWXGYts0ZM8C3F6t.xml"
+
+export const COMMUNITY_BLOGS: CommunityBlog[] = [
+ {
+ href: "https://vitalik.eth.limo/",
+ feed: VITALIK_FEED,
+ },
+ {
+ href: "https://blog.ethereum.org/",
+ feed: "https://blog.ethereum.org/en/feed.xml",
+ },
+ {
+ href: "https://ethpandaops.io/posts/",
+ feed: "https://ethpandaops.io/posts/index.xml",
+ },
+ {
+ href: "https://ethstaker.cc/blog",
+ feed: "https://paragraph.xyz/api/blogs/rss/@ethstaker",
+ },
+ {
+ href: "https://0xparc.org/blog",
+ feed: _0X_PARC_FEED,
+ },
+ {
+ href: "https://www.attestant.io/posts/",
+ feed: "https://www.attestant.io/posts/",
+ },
+ { name: "Devcon", href: "https://devcon.org/en/blogs/" },
+ {
+ href: "https://soliditylang.org/blog/",
+ feed: SOLIDITY_FEED,
+ },
+ {
+ href: "https://mirror.xyz/privacy-scaling-explorations.eth",
+ feed: "https://mirror.xyz/privacy-scaling-explorations.eth/feed/atom",
+ },
+ {
+ href: "https://stark.mirror.xyz/",
+ feed: "https://stark.mirror.xyz/feed/atom",
+ },
+]
+
+export const BLOG_FEEDS = COMMUNITY_BLOGS.map(({ feed }) => feed).filter(
+ Boolean
+) as string[]
+
+export const BLOGS_WITHOUT_FEED = COMMUNITY_BLOGS.filter((item) => !item.feed)
diff --git a/src/lib/types.ts b/src/lib/types.ts
index b7faea71248..852fdd3cc9a 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,10 +1,10 @@
import type { Options } from "mdast-util-toc"
import type { NextPage } from "next"
import type { AppProps } from "next/app"
-import { StaticImageData } from "next/image"
-import { SSRConfig } from "next-i18next"
+import type { StaticImageData } from "next/image"
+import type { SSRConfig } from "next-i18next"
import type { ReactElement, ReactNode } from "react"
-import { Icon } from "@chakra-ui/react"
+import type { Icon } from "@chakra-ui/react"
import type {
DocsFrontmatter,
@@ -18,7 +18,7 @@ import type {
import type { BreadcrumbsProps } from "@/components/Breadcrumbs"
import type { CallToActionProps } from "@/components/Hero/CallToAction"
-import { SimulatorNav } from "@/components/Simulator/interfaces"
+import type { SimulatorNav } from "@/components/Simulator/interfaces"
import allQuizData from "@/data/quizzes"
import allQuestionData from "@/data/quizzes/questionBank"
@@ -26,12 +26,15 @@ import allQuestionData from "@/data/quizzes/questionBank"
import { WALLETS_FILTERS_DEFAULT } from "./constants"
import { layoutMapping } from "@/pages/[...slug]"
+import twConfig from "@/styles/config"
// Credit: https://stackoverflow.com/a/52331580
export type Unpacked = T extends (infer U)[] ? U : T
export type ChildOnlyProp = { children?: ReactNode }
+export type ClassNameProp = { className?: string }
+
export type NextPageWithLayout