Users, node operators, app developers, core developers, and any other stakeholders
- are invited to voice their support for their preferred headliner proposals for the Hegota upgrade.
+ are invited to voice their support for their preferred proposals for the {displayName} upgrade.
- Drag and drop (desktop) or tap-to-assign (mobile) the headliner proposals
+ Drag and drop (desktop) or tap-to-assign (mobile) the proposals
into tiers. S-tier represents your highest priority proposals,
while D-tier represents your lowest priority.
Download the image to share your rankings and start a conversation.{" "}
- Learn more about Hegota
+ Learn more about {displayName}
.
-
-
-
-
-
+ {tierConfig?.deadline && (
+
+
+
+ The deadline for headliner proposal submissions was {tierConfig.deadline}.
+
-
- The deadline for headliner proposal submissions was February 4th, 2025.
-
-
+ )}
)}
@@ -769,7 +797,7 @@ const RankPage: React.FC = () => {
-
-
- Headliner Proposals
-
- ({getUnassignedItems().length} unranked)
-
-
- {items.filter((item) => item.tier !== null).length > 0 && (
-
- Ready to generate image
-
+
+
+
+ Proposals
+
+ ({getUnassignedItems().length} unranked)
+
+
+ {visibleItems.filter((item) => item.tier !== null).length > 0 && (
+
+ Ready to generate image
+
+ )}
+
+ {items.some((item) => item.headliner) && (
+
+ setIncludeHeadliners(e.target.checked)}
+ className="rounded border-slate-300 text-purple-600 focus:ring-purple-500 dark:border-slate-600 dark:bg-slate-700"
+ />
+ Include headliner proposals
+
)}
@@ -1127,7 +1168,7 @@ const RankPage: React.FC = () => {
{hoveredItem.eip && (
fork.forkName.toLowerCase() === "hegota")?.champions}
+ champions={hoveredItem.eip.forkRelationships.find(fr => fr.forkName.toLowerCase() === fork)?.champions}
/>
)}
{hoveredItem.pendingProposal && hoveredItem.pendingProposal.champions.length > 0 && (
diff --git a/src/components/network-upgrade/TierMakerLink.tsx b/src/components/network-upgrade/TierMakerLink.tsx
new file mode 100644
index 00000000..91b91649
--- /dev/null
+++ b/src/components/network-upgrade/TierMakerLink.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { forkHasTierMaker } from '../../utils/tierMaker';
+
+interface TierMakerLinkProps {
+ /** Lowercase upgrade id, e.g. 'hegota'. */
+ forkId: string;
+}
+
+/**
+ * Inline header link to a fork's proposal tier maker (`/rank/{fork}`). Renders
+ * nothing for forks without a tier maker, so it is safe to drop into any upgrade
+ * page header. Styled to match the adjacent "View Meta EIP Discussion" link.
+ */
+export const TierMakerLink: React.FC = ({ forkId }) => {
+ const id = forkId.toLowerCase();
+ if (!forkHasTierMaker(id)) return null;
+
+ return (
+
+ Rank the proposals
+
+
+
+
+ );
+};
diff --git a/src/components/network-upgrade/VoiceYourSupportCTA.tsx b/src/components/network-upgrade/VoiceYourSupportCTA.tsx
index 9d2edcca..8cddab53 100644
--- a/src/components/network-upgrade/VoiceYourSupportCTA.tsx
+++ b/src/components/network-upgrade/VoiceYourSupportCTA.tsx
@@ -25,7 +25,7 @@ export const VoiceYourSupportCTA: React.FC = ({ forkNa
Create Your Ranking
diff --git a/src/components/network-upgrade/index.ts b/src/components/network-upgrade/index.ts
index 3c6d3b31..762ec37c 100644
--- a/src/components/network-upgrade/index.ts
+++ b/src/components/network-upgrade/index.ts
@@ -10,4 +10,5 @@ export * from './PectraTimeline';
export * from './HeadlinerOptions';
export * from './NetworkUpgradeTimeline';
export * from './OverviewSection';
-export * from './TableOfContents';
\ No newline at end of file
+export * from './TableOfContents';
+export * from './TierMakerLink';
\ No newline at end of file
diff --git a/src/domain/routes/pageMetadata.ts b/src/domain/routes/pageMetadata.ts
index 81428b4a..fe13d0fb 100644
--- a/src/domain/routes/pageMetadata.ts
+++ b/src/domain/routes/pageMetadata.ts
@@ -37,8 +37,8 @@ export const staticPageMetadata = {
description: 'Key decisions from Ethereum AllCoreDevs meetings.',
},
rank: {
- title: 'Headliner Rankings - Forkcast',
- description: 'Rank and compare headliner proposals for upcoming Ethereum network upgrades.',
+ title: 'Proposal Rankings - Forkcast',
+ description: 'Rank and compare proposals for upcoming Ethereum network upgrades.',
},
eipsIndex: {
title: 'EIP Directory - Forkcast',
diff --git a/src/pages/rank.astro b/src/pages/rank.astro
deleted file mode 100644
index e51419cc..00000000
--- a/src/pages/rank.astro
+++ /dev/null
@@ -1,9 +0,0 @@
----
-import Layout from '../layouts/Layout.astro';
-import RankPage from '../components/RankPage';
-import { staticPageMetadata } from '../domain/routes/pageMetadata';
----
-
-
-
-
diff --git a/src/pages/rank/[fork].astro b/src/pages/rank/[fork].astro
new file mode 100644
index 00000000..8fc08d33
--- /dev/null
+++ b/src/pages/rank/[fork].astro
@@ -0,0 +1,16 @@
+---
+import Layout from '../../layouts/Layout.astro';
+import RankPage from '../../components/RankPage';
+import { staticPageMetadata } from '../../domain/routes/pageMetadata';
+import { getTierMakerForkIds } from '../../utils/tierMaker';
+
+export function getStaticPaths() {
+ return getTierMakerForkIds().map((fork) => ({ params: { fork } }));
+}
+
+const { fork } = Astro.params;
+---
+
+
+
+
diff --git a/src/utils/eip.test.ts b/src/utils/eip.test.ts
index 89d5881a..735460ec 100644
--- a/src/utils/eip.test.ts
+++ b/src/utils/eip.test.ts
@@ -6,6 +6,7 @@ import {
getStageAbbreviation,
getSummaryDescription,
getUpgradeAnchorExpansionState,
+ isForkInclusionCandidate,
isPendingEip,
} from './eip';
import type { EIP } from '../types';
@@ -70,6 +71,36 @@ describe('inclusion stage labels', () => {
});
});
+describe('isForkInclusionCandidate', () => {
+ const withStatus = (forkName: string, ...statuses: string[]): EIP =>
+ makeEip({
+ forkRelationships: [
+ {
+ forkName,
+ wasHeadlinerCandidate: false,
+ isHeadliner: false,
+ statusHistory: statuses.map((status) => ({ status, call: null, date: null })),
+ },
+ ] as EIP['forkRelationships'],
+ });
+
+ it('includes EIPs in the inclusion funnel (proposed and beyond)', () => {
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Proposed'), 'hegota')).toBe(true);
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Considered'), 'hegota')).toBe(true);
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Scheduled'), 'hegota')).toBe(true);
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Included'), 'hegota')).toBe(true);
+ });
+
+ it('excludes declined, withdrawn, and other forks', () => {
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Declined'), 'hegota')).toBe(false);
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Withdrawn'), 'hegota')).toBe(false);
+ // Latest status wins: proposed then declined is not a candidate.
+ expect(isForkInclusionCandidate(withStatus('Hegota', 'Proposed', 'Declined'), 'hegota')).toBe(false);
+ // No relationship for the requested fork.
+ expect(isForkInclusionCandidate(withStatus('Glamsterdam', 'Proposed'), 'hegota')).toBe(false);
+ });
+});
+
describe('getEipIdFromHash', () => {
it('parses EIP anchor hashes', () => {
expect(getEipIdFromHash('#eip-8011')).toBe(8011);
diff --git a/src/utils/eip.ts b/src/utils/eip.ts
index cb0332ee..72c40b8a 100644
--- a/src/utils/eip.ts
+++ b/src/utils/eip.ts
@@ -56,6 +56,21 @@ export const getInclusionStage = (eip: EIP, forkName?: string): InclusionStage =
return INCLUSION_STAGE_BY_STATUS[status] ?? 'Unknown';
};
+/**
+ * Whether an EIP is a live inclusion candidate for a fork: it entered the
+ * inclusion funnel (Proposed/Considered/Scheduled/Included) and was not later
+ * declined or withdrawn. This is the set the tier maker lets people rank.
+ */
+export const isForkInclusionCandidate = (eip: EIP, forkName?: string): boolean => {
+ const stage = getInclusionStage(eip, forkName);
+ return (
+ stage === 'Proposed for Inclusion' ||
+ stage === 'Considered for Inclusion' ||
+ stage === 'Scheduled for Inclusion' ||
+ stage === 'Included'
+ );
+};
+
/**
* Get the compact display label for an inclusion stage.
*/
diff --git a/src/utils/index.ts b/src/utils/index.ts
index b66a14fb..56fbbd0d 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,4 +1,5 @@
export * from './eip';
+export * from './tierMaker';
export * from './colors';
export * from './markdown';
export * from './timeline';
diff --git a/src/utils/tierMaker.ts b/src/utils/tierMaker.ts
new file mode 100644
index 00000000..af291128
--- /dev/null
+++ b/src/utils/tierMaker.ts
@@ -0,0 +1,23 @@
+// Forks with a public proposal tier maker at `/rank/{fork}`. Adding one here
+// gives it a tier-maker page and an upgrade-page header link automatically; add
+// a fork once it is actively collecting proposals for inclusion.
+// `deadline`: optional headliner submission deadline shown in the info panel.
+interface TierMakerConfig {
+ deadline?: string;
+}
+
+const TIER_MAKER_FORKS: Record
= {
+ hegota: { deadline: 'February 4th, 2025' },
+ glamsterdam: {},
+};
+
+/** Whether a fork (by upgrade id, case-insensitive) has a tier maker. */
+export const forkHasTierMaker = (forkName: string): boolean =>
+ forkName.toLowerCase() in TIER_MAKER_FORKS;
+
+/** Lowercase upgrade ids of every fork that has a tier maker. */
+export const getTierMakerForkIds = (): string[] => Object.keys(TIER_MAKER_FORKS);
+
+/** Tier-maker config for a fork, or undefined when it has none. */
+export const getTierMakerConfig = (forkName: string): TierMakerConfig | undefined =>
+ TIER_MAKER_FORKS[forkName.toLowerCase()];