Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion components/assessment/AssessmentApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {

<SiteFooter contentWidth={1440} />


{share && a.stage != null && (
<ShareModal
answers={a.answers}
Expand Down
11 changes: 5 additions & 6 deletions components/assessment/Blockers.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SLICES, blockers } from "@lib/rubric";
import { blockers } from "@lib/rubric";
import type { Answers, Stage } from "@lib/rubric/types";
import {
BlockNames,
Expand All @@ -19,14 +19,15 @@ export function Blockers({ answers, stage }: BlockersProps) {
if (stage === 2) {
return (
<BlockText win>
Every slice green — maximum resilience. Nothing can slash you or take you offline.
Every slice green — maximum resilience. No single failure can slash you or take
you offline.
</BlockText>
);
}
const target = stage === 0 ? "Stage 1" : "Stage 2";
return (
<BlockText>
<b>{list.length}</b> {list.length === 1 ? "win" : "wins"} from {target}:{" "}
<b>{list.length}</b> {list.length === 1 ? "win" : "wins"} away from {target}:{" "}
<BlockNames>{list.map((s) => s.short).join(", ")}</BlockNames>
</BlockText>
);
Expand All @@ -43,7 +44,7 @@ export function Legend() {
<RiskDot color="green" size="sm" /> Green · no single point of failure
</LegendItem>
<LegendItem>
<RiskDot color="yellow" size="sm" /> Yellow · partial
<RiskDot color="yellow" size="sm" /> Yellow · partially mitigated
</LegendItem>
<LegendItem>
<RiskDot color="red" size="sm" /> Red · single point of failure
Expand All @@ -68,5 +69,3 @@ export function pizzaOrigin(sliceIndex: number | null): { x: number; y: number }
y: (r.top + r.height / 2) / window.innerHeight,
};
}

export { SLICES };
65 changes: 22 additions & 43 deletions components/assessment/Results.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { forwardRef, useEffect, useRef, useState } from "react";
import { Pizza } from "@components/pizza/Pizza";
import { VbButton } from "@components/ui/VbButton";
import { SHARE_NAME_MAX, sharePreviewPath } from "@constants/index";
import { SHARE_NAME_MAX } from "@constants/index";
import { downloadElementAsPng } from "@lib/share/download-image";
import { SLICES, getTip, shareCode } from "@lib/rubric";
import { SLICES, STAGE_META, getTip, shareCode } from "@lib/rubric";
import type { Answers, SliceColor, SliceId, Stage } from "@lib/rubric/types";
import type { SliceMeta } from "@lib/rubric/types";
import {
Expand Down Expand Up @@ -60,53 +60,33 @@ import {
risk,
} from "./stitches";

const STAGE = {
0: {
name: "Stage 0",
kind: "Getting started",
tone: "red" as const,
line: "Every operator starts here. Clear the items in red below to reach Stage 1 — where no single failure can expose you to slashing.",
},
1: {
name: "Stage 1",
kind: "Safety",
tone: "yellow" as const,
line: "No single failure can expose you to slashing. One more climb to Stage 2 — where no single point of failure can take you offline or censor you either.",
},
2: {
name: "Stage 2",
kind: "Liveness",
tone: "green" as const,
line: "No single point of failure — your validator can't be slashed, stopped, or censored. You're upholding Ethereum's core values: decentralization, credible neutrality, and censorship resistance.",
},
} as const;
/** Longer result-hero copy per stage; naming comes from STAGE_META. */
const STAGE_LINE: Record<Stage, string> = {
0: "Every operator starts here. Clear the items in red below to reach Stage 1 — where no single failure can expose you to slashing.",
1: "No single failure can expose you to slashing. One more climb to Stage 2 — where no single point of failure can take you offline either.",
2: "No single point of failure — no single compromise can slash you, and no single outage can stop you. You're upholding Ethereum's core values: decentralization, credible neutrality, and censorship resistance.",
};

const WHY_MAXED: Record<SliceId, string> = {
keyCustody: "No single compromise can sign with your stake.",
clientDiversity: "No supermajority-client fork can drag you in.",
infraDiversity: "No single provider can take your validator offline.",
osDiversity: "No single OS bug can stop your validator.",
cpuDiversity: "No single CPU-architecture supply-chain risk.",
geoDiversity: "No single jurisdiction can stop or censor you.",
};

const SHARE_LINE: Record<Stage, string> = {
0: "Has a single point of failure — for now.",
1: "Safe from slashing — no single failure can get it slashed.",
2: "Maximum resilience — can't be slashed, stopped, or censored.",
osDiversity: "No single OS compromise can reach all of your keys.",
cpuDiversity: "No single CPU-level flaw can reach all of your keys.",
geoDiversity: "No single region's outage can take your validator offline.",
};

type ResultHeroProps = { stage: Stage; answers: Answers; ownerName?: string };

export function ResultHero({ stage, answers, ownerName }: ResultHeroProps) {
const m = STAGE[stage];
const m = STAGE_META[stage];
const tone = risk[m.tone];
const greens = SLICES.filter((s) => answers[s.id] === "green").length;
const outOfRed = SLICES.filter((s) => answers[s.id] && answers[s.id] !== "red").length;
const progress =
stage === 2
? "All six dimensions maxed out"
: `${outOfRed} of 6 dimensions secured · ${greens} maxed`;
? "All six slices maxed out"
: `${outOfRed} of 6 slices secured · ${greens} maxed`;

return (
<HeroCard tone={m.tone}>
Expand All @@ -118,7 +98,7 @@ export function ResultHero({ stage, answers, ownerName }: ResultHeroProps) {
<HeroStageKind css={{ color: tone, borderColor: tone }}>{m.kind}</HeroStageKind>
</HeroStageLine>
<HeroProgress>{progress}</HeroProgress>
<HeroLine>{m.line}</HeroLine>
<HeroLine>{STAGE_LINE[stage]}</HeroLine>
</HeroCard>
);
}
Expand Down Expand Up @@ -202,7 +182,7 @@ type ShareCardProps = {

export const ShareCard = forwardRef<HTMLDivElement, ShareCardProps>(
function ShareCard({ answers, stage, shareUrl }, ref) {
const m = STAGE[stage];
const m = STAGE_META[stage];
const tone = risk[m.tone];
return (
<ShareCardRoot ref={ref}>
Expand All @@ -225,7 +205,7 @@ export const ShareCard = forwardRef<HTMLDivElement, ShareCardProps>(
<ShareResk>My validator setup is</ShareResk>
<ShareStage css={{ color: tone }}>{m.name}</ShareStage>
<ShareKind css={{ color: tone, borderColor: tone }}>{m.kind}</ShareKind>
<ShareLine>{SHARE_LINE[stage]}</ShareLine>
<ShareLine>{m.shareLine}</ShareLine>
</ShareMeta>
</ShareBody>
<ShareFoot>How resilient is your validator? Find out →</ShareFoot>
Expand Down Expand Up @@ -256,7 +236,6 @@ export function ShareModal({
const [nameError, setNameError] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const code = shareCode(answers);
const previewPath = sharePreviewPath(code);

const handleNameChange = (value: string) => {
if (value.length > SHARE_NAME_MAX) {
Expand Down Expand Up @@ -284,11 +263,11 @@ export function ShareModal({
const postToX = () => {
const who = ownerName.trim();
const line = who
? `${who}'s validator setup is ${STAGE[stage].name} on Validator Beat. How resilient is yours?`
: `My validator setup is ${STAGE[stage].name} on Validator Beat. How resilient is yours?`;
? `${who}'s validator setup is ${STAGE_META[stage].name} on Validator Beat. How resilient is yours?`
: `My validator setup is ${STAGE_META[stage].name} on Validator Beat. How resilient is yours?`;
const text = encodeURIComponent(line);
window.open(
`https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(shareUrl)}`,
`https://x.com/intent/post?text=${text}&url=${encodeURIComponent(shareUrl)}`,
"_blank",
"noopener,noreferrer",
);
Expand Down Expand Up @@ -355,8 +334,8 @@ export function ShareModal({
</VbButton>
</ModalActions>
<ModalNote>
A preview image is generated per result at <b>{previewPath}</b> — anyone who
opens it sees your pizza, no data stored.
The link encodes your six colors (<b>{code}</b>) — anyone who opens it sees
this exact result. Nothing is stored.
</ModalNote>
</ModalInner>
</ModalOverlay>
Expand Down
76 changes: 30 additions & 46 deletions components/assessment/StageLadder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { STAGE_META } from "@lib/rubric";
import type { Stage } from "@lib/rubric/types";
import {
Ladder,
Expand All @@ -12,29 +13,7 @@ import {
LadderTag,
} from "./stitches";

const LADDER = [
{
n: 0 as const,
name: "Stage 0",
kind: "Getting started",
tag: "Has a single point of failure",
tone: "red" as const,
},
{
n: 1 as const,
name: "Stage 1",
kind: "Safety",
tag: "No single failure can get you slashed",
tone: "yellow" as const,
},
{
n: 2 as const,
name: "Stage 2",
kind: "Liveness",
tag: "No single failure can stop or censor you",
tone: "green" as const,
},
];
const STAGE_ORDER: Stage[] = [0, 1, 2];

type StageLadderProps = {
stage: Stage | null;
Expand All @@ -44,29 +23,34 @@ type StageLadderProps = {
export function StageLadder({ stage, vertical = false }: StageLadderProps) {
return (
<Ladder vertical={vertical}>
{LADDER.map((s, i) => (
<React.Fragment key={s.n}>
<LadderCell
tone={s.tone}
current={stage === s.n}
passed={stage != null && stage > s.n}
vertical={vertical}
>
<LadderRow>
<LadderName tone={s.tone}>{s.name}</LadderName>
<LadderKind tone={s.tone}>{s.kind}</LadderKind>
{stage === s.n && <LadderHere tone={s.tone}>you&apos;re here</LadderHere>}
{stage != null && stage > s.n && (
<LadderCheck tone={s.tone}>✓</LadderCheck>
)}
</LadderRow>
<LadderTag>{s.tag}</LadderTag>
</LadderCell>
{i < 2 && (
<LadderArrow vertical={vertical}>{vertical ? "↓" : "→"}</LadderArrow>
)}
</React.Fragment>
))}
{STAGE_ORDER.map((n, i) => {
const meta = STAGE_META[n];
return (
<React.Fragment key={n}>
<LadderCell
tone={meta.tone}
current={stage === n}
passed={stage != null && stage > n}
vertical={vertical}
>
<LadderRow>
<LadderName tone={meta.tone}>{meta.name}</LadderName>
<LadderKind tone={meta.tone}>{meta.kind}</LadderKind>
{stage === n && (
<LadderHere tone={meta.tone}>you&apos;re here</LadderHere>
)}
{stage != null && stage > n && (
<LadderCheck tone={meta.tone}>✓</LadderCheck>
)}
</LadderRow>
<LadderTag>{meta.tagline}</LadderTag>
</LadderCell>
{i < STAGE_ORDER.length - 1 && (
<LadderArrow vertical={vertical}>{vertical ? "↓" : "→"}</LadderArrow>
)}
</React.Fragment>
);
})}
</Ladder>
);
}
Loading
Loading