Repository: SiaFoundation/web
Pull request #980: feat(explorer): add v2 hardfork countdowns to home page
Author: login: telestrial
Created: 2025-03-06T22:51:11Z | Merged: 2025-03-14T16:25:29Z



Diff:
diff --git a/.changeset/strong-teachers-fly.md b/.changeset/strong-teachers-fly.md
new file mode 100644
index 000000000..672afc3f4
--- /dev/null
+++ b/.changeset/strong-teachers-fly.md
@@ -0,0 +1,5 @@
+---
+'explorer': minor
+---
+
+Added v2 hardfork countdowns to home page.
diff --git a/apps/explorer/components/HardforkCountdown/index.tsx b/apps/explorer/components/HardforkCountdown/index.tsx
new file mode 100644
index 000000000..ede650fd6
--- /dev/null
+++ b/apps/explorer/components/HardforkCountdown/index.tsx
@@ -0,0 +1,74 @@
+import { Text } from '@siafoundation/design-system'
+import { to } from '@siafoundation/request'
+import { getExplored } from '../../lib/explored'
+import clsx from 'clsx'
+
+function LearnMore({ color }: { color: string }) {
+  return (
+    <a
+      href="https://docs.sia.tech/navigating-the-v2-hardfork/v2"
+      target="_blank"
+      rel="noreferrer"
+      className={clsx(
+        'rounded py-1 px-2 inline-block font-sans text-white dark:text-graydark-50',
+        'font-normal text-sm inline-flex items-center gap-1 cursor-pointer',
+        color
+      )}
+    >
+      Learn more
+    </a>
+  )
+}
+
+export async function HardforkCountdown({ network }: { network: string }) {
+  const isMainnet = network === 'mainnet'
+  const [chainIndex, chainIndexError] = await to(getExplored().consensusTip())
+  if (chainIndexError || !chainIndex)
+    throw new Error('explored consensusTip request failed in HardforkCountdown')
+
+  const allowHeight = isMainnet ? 526000 : 112000
+  const allowTime = new Date(
+    isMainnet ? '2025-06-06T02:00:00.000Z' : '2025-03-01T07:00:00.000Z'
+  )
+
+  const requireHeight = isMainnet ? 530000 : 114000
+  const requireTime = new Date(
+    isMainnet ? '2025-07-04T02:00:00.000Z' : '2025-03-15T06:00:00.000Z'
+  )
+
+  const linkColor = isMainnet
+    ? 'bg-green-500 dark:bg-green-400'
+    : 'bg-amber-500 dark:bg-amber-400'
+
+  const currentTime = new Date()
+  const isRequireTimeActive = currentTime >= allowTime
+  const selectedTime = isRequireTimeActive ? requireTime : allowTime
+  const selectedHeight = isRequireTimeActive ? requireHeight : allowHeight
+
+  const daysRemaining = Math.ceil(
+    (selectedTime.getTime() - currentTime.getTime()) / (1000 * 60 * 60 * 24)
+  )
+
+  if (chainIndex.height >= requireHeight) {
+    return (
+      <div className="w-full py-3 flex gap-2 justify-center items-center bg-gray-100 dark:bg-graydark-50">
+        <Text color="contrast" weight="medium">
+          The V2 Hardfork update is now required on{' '}
+          {isMainnet ? 'mainnet' : 'the zen network'}
+        </Text>
+        <LearnMore color={linkColor} />
+      </div>
+    )
+  }
+
+  return (
+    <div className="w-full py-3 flex gap-2 justify-center items-center bg-gray-100 dark:bg-graydark-50">
+      <Text color="contrast" weight="medium">
+        V2 Hardfork {isRequireTimeActive ? 'is required' : 'activates'} on{' '}
+        {isMainnet ? 'mainnet' : 'the zen network'} in {daysRemaining} days at
+        block height {selectedHeight.toLocaleString()}
+      </Text>
+      <LearnMore color={linkColor} />
+    </div>
+  )
+}
diff --git a/apps/explorer/components/Layout/index.tsx b/apps/explorer/components/Layout/index.tsx
index 740ce9e21..d6c801de6 100644
--- a/apps/explorer/components/Layout/index.tsx
+++ b/apps/explorer/components/Layout/index.tsx
@@ -6,6 +6,7 @@ import { isMainnet, network, networkName, appName } from '../../config'
 import { NavDropdownMenu } from './NavDropdownMenu'
 import { Footer } from './Footer'
 import { Navbar } from './Navbar'
+import { HardforkCountdown } from '../HardforkCountdown'
 
 type Props = {
   children: React.ReactNode
@@ -15,6 +16,7 @@ export function Layout({ children }: Props) {
   return (
     <div className="relative h-full bg-gray-100 dark:bg-graydark-50 overflow-hidden">
       <div className="relative z-10 h-full overflow-y-auto">
+        <HardforkCountdown network={network} />
         <Navbar
           appName={appName}
           homeHref={routes.home.index}


Reviews:
user: login: peterjan
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-07T10:01:47Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:13:16Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:18:48Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T18:43:34Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T20:43:53Z
user: login: peterjan
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T08:35:22Z
user: login: chris124567
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T14:15:46Z
user: login: peterjan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T08:40:33Z
user: login: peterjan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T08:41:03Z
user: login: n8mgr
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-12T23:06:40Z
user: login: peterjan
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T09:25:25Z

Review comments:
diff_hunk: @@ -0,0 +1,5 @@
+---
+'explorer': minor
+---
+
+Added v2 hardfork countdowns to home page.
path: .changeset/strong-teachers-fly.md
user: login: peterjan
body: `nit` use imperative mood for changeset and commit messages
created_at: 2025-03-07T09:48:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,108 @@
+import { ClientSideOnly, Skeleton, Text } from '@siafoundation/design-system'
+import { useCallback, useEffect, useState } from 'react'
+
+type Props = {
+  title: 'Require' | 'Allow'
+  blockHeight: string
+  date: string
+}
+
+export function HardforkCountdownItem({ title, blockHeight, date }: Props) {
+  const targetDate = new Date(date).getTime()
+
+  const calculateTimeLeft = useCallback(() => {
+    return Math.max(targetDate - new Date().getTime(), 0)
+  }, [targetDate])
+
+  const [countdownMS, setCountdownMS] = useState(calculateTimeLeft())
+
+  useEffect(() => {
+    if (countdownMS === 0) return
+
+    const interval = setInterval(() => {
+      setCountdownMS(calculateTimeLeft())
+    }, 1000)
+
+    return () => clearInterval(interval)
+  }, [countdownMS, calculateTimeLeft])
+
+  const padToTwoDigits = (time: number) => time.toString().padStart(2, '0')
path: apps/explorer/components/HardforkCountdown/HardforkCountdownItem.tsx
user: login: peterjan
body: I would pull this out of the component. No need to recreate this every time.
created_at: 2025-03-07T09:50:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,108 @@
+import { ClientSideOnly, Skeleton, Text } from '@siafoundation/design-system'
+import { useCallback, useEffect, useState } from 'react'
+
+type Props = {
+  title: 'Require' | 'Allow'
+  blockHeight: string
+  date: string
+}
+
+export function HardforkCountdownItem({ title, blockHeight, date }: Props) {
+  const targetDate = new Date(date).getTime()
path: apps/explorer/components/HardforkCountdown/HardforkCountdownItem.tsx
user: login: peterjan
body: This can be state `const [targetDate] = useState(new Date(date).getTime())`
created_at: 2025-03-07T09:51:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,108 @@
+import { ClientSideOnly, Skeleton, Text } from '@siafoundation/design-system'
+import { useCallback, useEffect, useState } from 'react'
+
+type Props = {
+  title: 'Require' | 'Allow'
+  blockHeight: string
+  date: string
+}
+
+export function HardforkCountdownItem({ title, blockHeight, date }: Props) {
+  const targetDate = new Date(date).getTime()
+
+  const calculateTimeLeft = useCallback(() => {
+    return Math.max(targetDate - new Date().getTime(), 0)
+  }, [targetDate])
+
+  const [countdownMS, setCountdownMS] = useState(calculateTimeLeft())
+
+  useEffect(() => {
+    if (countdownMS === 0) return
+
+    const interval = setInterval(() => {
+      setCountdownMS(calculateTimeLeft())
+    }, 1000)
+
+    return () => clearInterval(interval)
+  }, [countdownMS, calculateTimeLeft])
path: apps/explorer/components/HardforkCountdown/HardforkCountdownItem.tsx
user: login: peterjan
body: Do you need `countdownMS` here? It's been a while but I don't think you do. I think you end up resetting the interval on every render.
created_at: 2025-03-07T09:54:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,108 @@
+import { ClientSideOnly, Skeleton, Text } from '@siafoundation/design-system'
+import { useCallback, useEffect, useState } from 'react'
+
+type Props = {
+  title: 'Require' | 'Allow'
+  blockHeight: string
+  date: string
+}
+
+export function HardforkCountdownItem({ title, blockHeight, date }: Props) {
+  const targetDate = new Date(date).getTime()
+
+  const calculateTimeLeft = useCallback(() => {
+    return Math.max(targetDate - new Date().getTime(), 0)
+  }, [targetDate])
+
+  const [countdownMS, setCountdownMS] = useState(calculateTimeLeft())
+
+  useEffect(() => {
+    if (countdownMS === 0) return
+
+    const interval = setInterval(() => {
+      setCountdownMS(calculateTimeLeft())
+    }, 1000)
+
+    return () => clearInterval(interval)
+  }, [countdownMS, calculateTimeLeft])
+
+  const padToTwoDigits = (time: number) => time.toString().padStart(2, '0')
+
+  // Ensure values never go below 0
+  const days = padToTwoDigits(
path: apps/explorer/components/HardforkCountdown/HardforkCountdownItem.tsx
user: login: peterjan
body: ```js
  const secondsTotal = Math.floor(countdownMS / 1000)
  const days = padToTwoDigits(Math.floor(secondsTotal / (60 * 60 * 24)))
  const hours = padToTwoDigits(Math.floor((secondsTotal % (60 * 60 * 24)) / (60 * 60)))
  const minutes = padToTwoDigits(Math.floor((secondsTotal % (60 * 60)) / 60))
  const seconds = padToTwoDigits(secondsTotal % 60)
```

Could clean this up a bit
created_at: 2025-03-07T09:56:27Z
author_association: MEMBER
diff_hunk: @@ -226,62 +228,67 @@ export function Home({
   }, [metrics, blockHeight, exchange, totalHosts])
 
   return (
-    <ContentLayout
-      panel={
-        <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
-          {values.map(({ label, value }) => (
-            <div
-              className="flex flex-col gap-6 items-start overflow-hidden"
-              key={label}
-              data-testid="explorer-metrics-item"
+    <>
+      <HardforkCountdown network={network} />
+      <ContentLayout
+        panel={
+          <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
+            {values.map(({ label, value }) => (
+              <div
+                className="flex flex-col gap-6 items-start overflow-hidden"
+                key={label}
+                data-testid="explorer-metrics-item"
+              >
+                <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
+                  {label}
+                </Text>
+                {value}
+              </div>
+            ))}
+          </div>
+        }
+      >
+        <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
+          <div>
+            <BlockList
+              title="Latest blocks"
+              dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
+                height: block.height,
+                timestamp: block.timestamp,
+                href: routes.block.view.replace(':id', String(block.height)),
+              }))}
+            />
+          </div>
+          <div>
+            <EntityList
+              title="Top hosts"
+              actions={
+                <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
+                  <Information20 />
+                </Tooltip>
+              }
             >
-              <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
-                {label}
-              </Text>
-              {value}
-            </div>
-          ))}
-        </div>
-      }
-    >
-      <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
-        <div>
-          <BlockList
-            title="Latest blocks"
-            dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
-              height: block.height,
-              timestamp: block.timestamp,
-              href: routes.block.view.replace(':id', String(block.height)),
-            }))}
-          />
-        </div>
-        <div>
-          <EntityList
-            title="Top hosts"
-            actions={
-              <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
-                <Information20 />
-              </Tooltip>
-            }
-          >
-            {hosts
-              .filter((host) => (host.v2 ? host.rhpV4Settings : host.settings))
-              .map((host) => (
-                <HostListItem
-                  key={host.publicKey}
-                  host={host}
-                  exchange={exchange}
-                  entity={{
-                    label: getHostNetAddress(host),
-                    initials: 'H',
-                    avatar: hashToAvatar(host.publicKey),
-                    href: routes.host.view.replace(':id', host.publicKey),
-                  }}
-                />
-              ))}
-          </EntityList>
+              {hosts
+                .filter((host) =>
+                  host.v2 ? host.rhpV4Settings : host.settings
path: apps/explorer/components/Home/index.tsx
user: login: peterjan
body: `nit` we don't really use the term rhp4 settings but rather v2Settings 
created_at: 2025-03-07T10:00:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+import { Heading } from '@siafoundation/design-system'
+import { HardforkCountdownItem } from './HardforkCountdownItem'
+import { ContentLayout } from '../ContentLayout'
+import clsx from 'clsx'
+
+export function HardforkCountdown({ network }: { network: string }) {
+  return (
+    <ContentLayout
+      panel={
+        <div className="flex flex-col gap-10">
+          <div className="flex justify-between">
+            <Heading size="20" font="mono" ellipsis>
+              v2 Hardfork Countdown
+            </Heading>
+            <a
+              href="https://docs.sia.tech/navigating-the-v2-hardfork/v2"
+              target="_blank"
+              rel="noreferrer"
+              className={clsx(
+                'rounded py-0 px-2 hidden sm:flex inline-block font-sans text-white dark:text-graydark-50',
+                'font-normal text-sm inline-flex items-center gap-1 cursor-pointer',
+                network === 'mainnet'
+                  ? 'bg-green-500 dark:bg-green-400'
+                  : 'bg-amber-500 dark:bg-amber-400'
+              )}
+            >
+              Learn more
+            </a>
+          </div>
+          <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
+            <HardforkCountdownItem
+              title="Allow"
+              blockHeight={network === 'mainnet' ? '526,000' : '112,000'}
+              date={
+                network === 'mainnet'
+                  ? '2025-06-06T02:00:00.000Z'
path: apps/explorer/components/HardforkCountdown/index.tsx
user: login: peterjan
body: You could make this constant and move them out of the JSX, cleaning it up a bit.

```
const allowBlockHeight = network === 'mainnet' ? '526,000' : '112,000';
const requireBlockHeight = network === 'mainnet' ? '530,000' : '114,000';

const allowDate = network === 'mainnet' ? '2025-06-06T02:00:00.000Z' : '2025-03-01T07:00:00.000Z';
const requireDate = network === 'mainnet' ? '2025-07-04T02:00:00.000Z' : '2025-03-15T06:00:00.000Z';
```
created_at: 2025-03-07T10:01:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+---
+'explorer': minor
+---
+
+Added v2 hardfork countdowns to home page.
path: .changeset/strong-teachers-fly.md
user: login: telestrial
body: We use indicative mood for the changesets and imperative for the commits--at least in the web repo.
created_at: 2025-03-07T17:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,108 @@
+import { ClientSideOnly, Skeleton, Text } from '@siafoundation/design-system'
+import { useCallback, useEffect, useState } from 'react'
+
+type Props = {
+  title: 'Require' | 'Allow'
+  blockHeight: string
+  date: string
+}
+
+export function HardforkCountdownItem({ title, blockHeight, date }: Props) {
+  const targetDate = new Date(date).getTime()
path: apps/explorer/components/HardforkCountdown/HardforkCountdownItem.tsx
user: login: telestrial
body: It can be, but it doesn't need to be. It's a static value that never updates--the "now" that the component times away from. We use it in a state update but it does not need to be state.
created_at: 2025-03-07T17:18:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,108 @@
+import { ClientSideOnly, Skeleton, Text } from '@siafoundation/design-system'
+import { useCallback, useEffect, useState } from 'react'
+
+type Props = {
+  title: 'Require' | 'Allow'
+  blockHeight: string
+  date: string
+}
+
+export function HardforkCountdownItem({ title, blockHeight, date }: Props) {
+  const targetDate = new Date(date).getTime()
+
+  const calculateTimeLeft = useCallback(() => {
+    return Math.max(targetDate - new Date().getTime(), 0)
+  }, [targetDate])
+
+  const [countdownMS, setCountdownMS] = useState(calculateTimeLeft())
+
+  useEffect(() => {
+    if (countdownMS === 0) return
+
+    const interval = setInterval(() => {
+      setCountdownMS(calculateTimeLeft())
+    }, 1000)
+
+    return () => clearInterval(interval)
+  }, [countdownMS, calculateTimeLeft])
path: apps/explorer/components/HardforkCountdown/HardforkCountdownItem.tsx
user: login: telestrial
body: I've rewritten this. Your concern was valid. Now, we keep a reference to the interval outside the useEffect, which allows us to optionally invoke, stop, and handle when we reach 0.
created_at: 2025-03-07T18:43:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -226,62 +228,67 @@ export function Home({
   }, [metrics, blockHeight, exchange, totalHosts])
 
   return (
-    <ContentLayout
-      panel={
-        <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
-          {values.map(({ label, value }) => (
-            <div
-              className="flex flex-col gap-6 items-start overflow-hidden"
-              key={label}
-              data-testid="explorer-metrics-item"
+    <>
+      <HardforkCountdown network={network} />
+      <ContentLayout
+        panel={
+          <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
+            {values.map(({ label, value }) => (
+              <div
+                className="flex flex-col gap-6 items-start overflow-hidden"
+                key={label}
+                data-testid="explorer-metrics-item"
+              >
+                <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
+                  {label}
+                </Text>
+                {value}
+              </div>
+            ))}
+          </div>
+        }
+      >
+        <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
+          <div>
+            <BlockList
+              title="Latest blocks"
+              dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
+                height: block.height,
+                timestamp: block.timestamp,
+                href: routes.block.view.replace(':id', String(block.height)),
+              }))}
+            />
+          </div>
+          <div>
+            <EntityList
+              title="Top hosts"
+              actions={
+                <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
+                  <Information20 />
+                </Tooltip>
+              }
             >
-              <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
-                {label}
-              </Text>
-              {value}
-            </div>
-          ))}
-        </div>
-      }
-    >
-      <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
-        <div>
-          <BlockList
-            title="Latest blocks"
-            dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
-              height: block.height,
-              timestamp: block.timestamp,
-              href: routes.block.view.replace(':id', String(block.height)),
-            }))}
-          />
-        </div>
-        <div>
-          <EntityList
-            title="Top hosts"
-            actions={
-              <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
-                <Information20 />
-              </Tooltip>
-            }
-          >
-            {hosts
-              .filter((host) => (host.v2 ? host.rhpV4Settings : host.settings))
-              .map((host) => (
-                <HostListItem
-                  key={host.publicKey}
-                  host={host}
-                  exchange={exchange}
-                  entity={{
-                    label: getHostNetAddress(host),
-                    initials: 'H',
-                    avatar: hashToAvatar(host.publicKey),
-                    href: routes.host.view.replace(':id', host.publicKey),
-                  }}
-                />
-              ))}
-          </EntityList>
+              {hosts
+                .filter((host) =>
+                  host.v2 ? host.rhpV4Settings : host.settings
path: apps/explorer/components/Home/index.tsx
user: login: telestrial
body: If that's the case, my suggestion would be that we change this on the `explored` side. The solutions as far as remapping and retyping get complicated for DX pretty quickly, and there's no reason to go down those roads when when we own the server response data structure. Pinging @chris124567 for visibility.
created_at: 2025-03-07T20:43:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -226,62 +228,67 @@ export function Home({
   }, [metrics, blockHeight, exchange, totalHosts])
 
   return (
-    <ContentLayout
-      panel={
-        <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
-          {values.map(({ label, value }) => (
-            <div
-              className="flex flex-col gap-6 items-start overflow-hidden"
-              key={label}
-              data-testid="explorer-metrics-item"
+    <>
+      <HardforkCountdown network={network} />
+      <ContentLayout
+        panel={
+          <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
+            {values.map(({ label, value }) => (
+              <div
+                className="flex flex-col gap-6 items-start overflow-hidden"
+                key={label}
+                data-testid="explorer-metrics-item"
+              >
+                <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
+                  {label}
+                </Text>
+                {value}
+              </div>
+            ))}
+          </div>
+        }
+      >
+        <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
+          <div>
+            <BlockList
+              title="Latest blocks"
+              dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
+                height: block.height,
+                timestamp: block.timestamp,
+                href: routes.block.view.replace(':id', String(block.height)),
+              }))}
+            />
+          </div>
+          <div>
+            <EntityList
+              title="Top hosts"
+              actions={
+                <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
+                  <Information20 />
+                </Tooltip>
+              }
             >
-              <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
-                {label}
-              </Text>
-              {value}
-            </div>
-          ))}
-        </div>
-      }
-    >
-      <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
-        <div>
-          <BlockList
-            title="Latest blocks"
-            dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
-              height: block.height,
-              timestamp: block.timestamp,
-              href: routes.block.view.replace(':id', String(block.height)),
-            }))}
-          />
-        </div>
-        <div>
-          <EntityList
-            title="Top hosts"
-            actions={
-              <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
-                <Information20 />
-              </Tooltip>
-            }
-          >
-            {hosts
-              .filter((host) => (host.v2 ? host.rhpV4Settings : host.settings))
-              .map((host) => (
-                <HostListItem
-                  key={host.publicKey}
-                  host={host}
-                  exchange={exchange}
-                  entity={{
-                    label: getHostNetAddress(host),
-                    initials: 'H',
-                    avatar: hashToAvatar(host.publicKey),
-                    href: routes.host.view.replace(':id', host.publicKey),
-                  }}
-                />
-              ))}
-          </EntityList>
+              {hosts
+                .filter((host) =>
+                  host.v2 ? host.rhpV4Settings : host.settings
path: apps/explorer/components/Home/index.tsx
user: login: chris124567
body: Yes we changed this to v2Settings
created_at: 2025-03-10T14:15:46Z
author_association: MEMBER
diff_hunk: @@ -226,62 +228,67 @@ export function Home({
   }, [metrics, blockHeight, exchange, totalHosts])
 
   return (
-    <ContentLayout
-      panel={
-        <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
-          {values.map(({ label, value }) => (
-            <div
-              className="flex flex-col gap-6 items-start overflow-hidden"
-              key={label}
-              data-testid="explorer-metrics-item"
+    <>
+      <HardforkCountdown network={network} />
+      <ContentLayout
+        panel={
+          <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
+            {values.map(({ label, value }) => (
+              <div
+                className="flex flex-col gap-6 items-start overflow-hidden"
+                key={label}
+                data-testid="explorer-metrics-item"
+              >
+                <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
+                  {label}
+                </Text>
+                {value}
+              </div>
+            ))}
+          </div>
+        }
+      >
+        <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
+          <div>
+            <BlockList
+              title="Latest blocks"
+              dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
+                height: block.height,
+                timestamp: block.timestamp,
+                href: routes.block.view.replace(':id', String(block.height)),
+              }))}
+            />
+          </div>
+          <div>
+            <EntityList
+              title="Top hosts"
+              actions={
+                <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
+                  <Information20 />
+                </Tooltip>
+              }
             >
-              <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
-                {label}
-              </Text>
-              {value}
-            </div>
-          ))}
-        </div>
-      }
-    >
-      <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
-        <div>
-          <BlockList
-            title="Latest blocks"
-            dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
-              height: block.height,
-              timestamp: block.timestamp,
-              href: routes.block.view.replace(':id', String(block.height)),
-            }))}
-          />
-        </div>
-        <div>
-          <EntityList
-            title="Top hosts"
-            actions={
-              <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
-                <Information20 />
-              </Tooltip>
-            }
-          >
-            {hosts
-              .filter((host) => (host.v2 ? host.rhpV4Settings : host.settings))
-              .map((host) => (
-                <HostListItem
-                  key={host.publicKey}
-                  host={host}
-                  exchange={exchange}
-                  entity={{
-                    label: getHostNetAddress(host),
-                    initials: 'H',
-                    avatar: hashToAvatar(host.publicKey),
-                    href: routes.host.view.replace(':id', host.publicKey),
-                  }}
-                />
-              ))}
-          </EntityList>
+              {hosts
+                .filter((host) =>
+                  host.v2 ? host.rhpV4Settings : host.settings
path: apps/explorer/components/Home/index.tsx
user: login: peterjan
body: Unresolving because I think we might as well update the key in this PR? Feel free to resolve and merge though if not.
created_at: 2025-03-11T08:40:33Z
author_association: MEMBER
diff_hunk: @@ -226,62 +228,67 @@ export function Home({
   }, [metrics, blockHeight, exchange, totalHosts])
 
   return (
-    <ContentLayout
-      panel={
-        <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
-          {values.map(({ label, value }) => (
-            <div
-              className="flex flex-col gap-6 items-start overflow-hidden"
-              key={label}
-              data-testid="explorer-metrics-item"
+    <>
+      <HardforkCountdown network={network} />
+      <ContentLayout
+        panel={
+          <div className="grid grid-cols-2 md:grid-cols-3 gap-x-6 md:gap-x-12 gap-y-12">
+            {values.map(({ label, value }) => (
+              <div
+                className="flex flex-col gap-6 items-start overflow-hidden"
+                key={label}
+                data-testid="explorer-metrics-item"
+              >
+                <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
+                  {label}
+                </Text>
+                {value}
+              </div>
+            ))}
+          </div>
+        }
+      >
+        <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
+          <div>
+            <BlockList
+              title="Latest blocks"
+              dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
+                height: block.height,
+                timestamp: block.timestamp,
+                href: routes.block.view.replace(':id', String(block.height)),
+              }))}
+            />
+          </div>
+          <div>
+            <EntityList
+              title="Top hosts"
+              actions={
+                <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
+                  <Information20 />
+                </Tooltip>
+              }
             >
-              <Text color="subtle" scaleSize="14" className="w-full" ellipsis>
-                {label}
-              </Text>
-              {value}
-            </div>
-          ))}
-        </div>
-      }
-    >
-      <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-5">
-        <div>
-          <BlockList
-            title="Latest blocks"
-            dataset={reverse(sortBy(blocks, 'timestamp')).map((block) => ({
-              height: block.height,
-              timestamp: block.timestamp,
-              href: routes.block.view.replace(':id', String(block.height)),
-            }))}
-          />
-        </div>
-        <div>
-          <EntityList
-            title="Top hosts"
-            actions={
-              <Tooltip content="The Sia Foundation believes hosts can be evaluated in many different ways, depending on purpose. This list uses a combination of age, uptime, pricing, and used storage, with slight weight to the first two categories.">
-                <Information20 />
-              </Tooltip>
-            }
-          >
-            {hosts
-              .filter((host) => (host.v2 ? host.rhpV4Settings : host.settings))
-              .map((host) => (
-                <HostListItem
-                  key={host.publicKey}
-                  host={host}
-                  exchange={exchange}
-                  entity={{
-                    label: getHostNetAddress(host),
-                    initials: 'H',
-                    avatar: hashToAvatar(host.publicKey),
-                    href: routes.host.view.replace(':id', host.publicKey),
-                  }}
-                />
-              ))}
-          </EntityList>
+              {hosts
+                .filter((host) =>
+                  host.v2 ? host.rhpV4Settings : host.settings
path: apps/explorer/components/Home/index.tsx
user: login: peterjan
body: Lol, ok next PR for review was https://github.com/SiaFoundation/web/pull/981 - resolving 
created_at: 2025-03-11T08:41:03Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-03-06T22:51:13Z
body: [vc]: #2In0AlkR3whhZ0vOq3oDbMLRyUZ0FRi5ca4MzxXZEcY=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJleHBsb3JlciIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zaWEtZm91bmRhdGlvbi9leHBsb3Jlci8zUHN5aVZkTExRNjJYWUozUTJoQVNtdGV1Rzd4IiwicHJldmlld1VybCI6ImV4cGxvcmVyLWdpdC0wMy0wNi1mZWF0ZXhwbG9yZXJhZGR2MmhhcmRmLTI2Zjk1MC1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjoxLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MSwicGFnZSI6Ii8iLCJsaW5rIjoiZXhwbG9yZXItZ2l0LTAzLTA2LWZlYXRleHBsb3JlcmFkZHYyaGFyZGYtMjZmOTUwLXNpYS1mb3VuZGF0aW9uLnZlcmNlbC5hcHAvIn19LHsibmFtZSI6InJlbnRlcmQiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2lhLWZvdW5kYXRpb24vcmVudGVyZC9BNHBDYjRjTWNkYWpXYm9KVWZHQzNyVEx1azN3IiwicHJldmlld1VybCI6InJlbnRlcmQtZ2l0LTAzLTA2LWZlYXRleHBsb3JlcmFkZHYyaGFyZGZvLTQxNmU1MS1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6Imhvc3RkIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NpYS1mb3VuZGF0aW9uL2hvc3RkL0ZoMUN2MnFwZFVnWjJtQm1Zd25tRW40MUV6SkwiLCJwcmV2aWV3VXJsIjoiaG9zdGQtZ2l0LTAzLTA2LWZlYXRleHBsb3JlcmFkZHYyaGFyZGZvcmstZDZlNDEwLXNpYS1mb3VuZGF0aW9uLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX0seyJuYW1lIjoid2Vic2l0ZSIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zaWEtZm91bmRhdGlvbi93ZWJzaXRlLzhGYlZjTjQ5ZXdxTHhIUjVCS3lQUEh2S1RTdDkiLCJwcmV2aWV3VXJsIjoid2Vic2l0ZS1naXQtMDMtMDYtZmVhdGV4cGxvcmVyYWRkdjJoYXJkZm8tYThkZGZjLXNpYS1mb3VuZGF0aW9uLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX0seyJuYW1lIjoiZXhwbG9yZXItemVuIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NpYS1mb3VuZGF0aW9uL2V4cGxvcmVyLXplbi80OXlMWkhUMnY5NFBMTmpBMURkUEpNbUNOUm5oIiwicHJldmlld1VybCI6ImV4cGxvcmVyLXplbi1naXQtMDMtMDYtZmVhdGV4cGxvcmVyYWRkdjJoLTU3ZjY5ZC1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImV4cGxvcmVyLXplbi1naXQtMDMtMDYtZmVhdGV4cGxvcmVyYWRkdjJoLTU3ZjY5ZC1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **explorer** | ✅ Ready ([Inspect](https://vercel.com/sia-foundation/explorer/3PsyiVdLLQ62XYJ3Q2hASmteuG7x)) | [Visit Preview](https://explorer-git-03-06-featexploreraddv2hardf-26f950-sia-foundation.vercel.app) | ✅ [1 resolved](https://vercel.live/open-feedback/explorer-git-03-06-featexploreraddv2hardf-26f950-sia-foundation.vercel.app?via=pr-comment-feedback-link&nextHash=show-comment-archive%3Dtrue) | Mar 14, 2025 4:17pm |
| **explorer-zen** | ✅ Ready ([Inspect](https://vercel.com/sia-foundation/explorer-zen/49yLZHT2v94PLNjA1DdPJMmCNRnh)) | [Visit Preview](https://explorer-zen-git-03-06-featexploreraddv2h-57f69d-sia-foundation.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/explorer-zen-git-03-06-featexploreraddv2h-57f69d-sia-foundation.vercel.app?via=pr-comment-feedback-link) | Mar 14, 2025 4:17pm |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **hostd** | ⬜️ Ignored ([Inspect](https://vercel.com/sia-foundation/hostd/Fh1Cv2qpdUgZ2mBmYwnmEn41EzJL)) | [Visit Preview](https://hostd-git-03-06-featexploreraddv2hardfork-d6e410-sia-foundation.vercel.app) |  | Mar 14, 2025 4:17pm |
| **renterd** | ⬜️ Ignored ([Inspect](https://vercel.com/sia-foundation/renterd/A4pCb4cMcdajWboJUfGC3rTLuk3w)) | [Visit Preview](https://renterd-git-03-06-featexploreraddv2hardfo-416e51-sia-foundation.vercel.app) |  | Mar 14, 2025 4:17pm |
| **website** | ⬜️ Ignored ([Inspect](https://vercel.com/sia-foundation/website/8FbVcN49ewqLxHR5BKyPPHvKTSt9)) | [Visit Preview](https://website-git-03-06-featexploreraddv2hardfo-a8ddfc-sia-foundation.vercel.app) |  | Mar 14, 2025 4:17pm |
</details>






author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-03-06T22:51:14Z
body: ###  🦋  Changeset detected

Latest commit: 25b4f7ef31a0034ef8805bb1b7808dff3cb343e5

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name     | Type  |
| -------- | ----- |
| explorer | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/SiaFoundation/web/new/03-06-feat_explorer_add_v2_hardfork_countdowns_to_home_page?filename=.changeset/healthy-jars-fail.md&value=---%0A%22explorer%22%3A%20patch%0A---%0A%0Afeat(explorer)%3A%20add%20v2%20hardfork%20countdowns%20to%20home%20page%0A)


author_association: NONE
user: login: telestrial
created_at: 2025-03-06T22:51:28Z
body: * **#980** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/980?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/980?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: CONTRIBUTOR
user: login: telestrial
created_at: 2025-03-14T16:25:15Z
body: ### Merge activity

* **Mar 14, 12:25 PM EDT**: A user started a stack merge that includes this pull request via [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/980).
* **Mar 14, 12:25 PM EDT**: A user merged this pull request with [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/980).

author_association: CONTRIBUTOR