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
13 changes: 13 additions & 0 deletions components/AppThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import {
ThemeProvider,
type ThemeProviderProps,
} from "next-themes";

export function AppThemeProvider({
children,
...props
}: ThemeProviderProps) {
return <ThemeProvider {...props}>{children}</ThemeProvider>;
}
17 changes: 7 additions & 10 deletions components/assessment/AssessmentApp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { Pizza } from "@components/pizza/Pizza";
import { useAssessment } from "@hooks/useAssessment";
Expand All @@ -18,6 +17,7 @@ import {
} from "./Blockers";
import { Intro } from "./Intro";
import { Question } from "./Question";
import { ThemeToggle } from "./ThemeToggle";
import { LevelUp, ResultHero, ShareModal } from "./Results";
import {
BrandAccent,
Expand Down Expand Up @@ -112,16 +112,13 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {
return (
<Shell>
<TopBar>
<Link href="/" passHref legacyBehavior>
<BrandLink>
Validator <BrandAccent>Beat</BrandAccent>
</BrandLink>
</Link>
<BrandLink href="/">
Validator <BrandAccent>Beat</BrandAccent>
</BrandLink>
<TagPill>v0.1 · self-assessment</TagPill>
<TopSpacer />
<Link href="/methodology/" passHref legacyBehavior>
<TopNavLink>Methodology</TopNavLink>
</Link>
<TopNavLink href="/methodology/">Methodology</TopNavLink>
<ThemeToggle />
</TopBar>

<MainGrid>
Expand All @@ -135,7 +132,7 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {
onChoose={choose}
index={a.step}
total={a.total}
onBack={a.back}
onBack={a.step === 0 ? a.toIntro : a.back}
onShowResults={
allAnswered(a.answers) ? a.showResults : undefined
}
Expand Down
2 changes: 1 addition & 1 deletion components/assessment/Question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function Question({
)}
</QuestionRisk>
)}
{index > 0 && onBack && (
{onBack && (
<BackButton type="button" onClick={onBack}>
← Back
</BackButton>
Expand Down
49 changes: 49 additions & 0 deletions components/assessment/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { ThemeToggleButton } from "./stitches";

const SunIcon = () => (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
);

const MoonIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
</svg>
);

/**
* Sun/moon button that flips light/dark. Renders the light-mode (moon) icon
* until mounted so it never mismatches the server-rendered markup.
*/
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);

useEffect(() => setMounted(true), []);

const isDark = mounted && resolvedTheme === "dark";

return (
<ThemeToggleButton
type="button"
onClick={() => setTheme(isDark ? "light" : "dark")}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
>
{mounted && isDark ? <SunIcon /> : <MoonIcon />}
</ThemeToggleButton>
);
}
26 changes: 22 additions & 4 deletions components/assessment/stitches.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box, Text, styled } from "@obolnetwork/obol-ui";
import type { SliceColor } from "@lib/rubric/types";
import NextLink from "next/link";

/** Validator Beat risk palette — maps to theme-tokens.css */
export const risk = {
Expand Down Expand Up @@ -46,7 +47,7 @@ export const TopBar = styled(Box, {
flexShrink: 0,
});

export const BrandLink = styled("a", {
export const BrandLink = styled(NextLink, {
fontSize: "$3",
fontWeight: "$bold",
letterSpacing: "-0.01em",
Expand All @@ -70,14 +71,29 @@ export const TagPill = styled(Box, {

export const TopSpacer = styled(Box, { marginLeft: "auto" });

export const TopNavLink = styled("a", {
export const TopNavLink = styled(NextLink, {
fontSize: "$2",
fontWeight: "$medium",
color: "$textMiddle",
textDecoration: "none",
"&:hover": { color: "$body" },
});

export const ThemeToggleButton = styled("button", {
all: "unset",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 30,
height: 30,
borderRadius: "$round",
color: "$textMiddle",
border: "1px solid $bg05",
"&:hover": { color: "$body", backgroundColor: "$bg04" },
"&:focus-visible": { outline: "2px solid var(--theme-brand)", outlineOffset: 2 },
});

export const MainGrid = styled(Box, {
flex: 1,
minHeight: 0,
Expand Down Expand Up @@ -262,12 +278,13 @@ export const LadderName = styled(Text, {
export const LadderKind = styled(Text, {
fontSize: "9.5px",
fontWeight: "$bold",
lineHeight: 1,
textTransform: "uppercase",
letterSpacing: "0.07em",
color: "$textMiddle",
border: "1px solid $bg05",
borderRadius: "$pill",
padding: "1px 7px",
padding: "3px 7px",
variants: {
tone: {
red: { color: risk.red, borderColor: risk.redB },
Expand All @@ -280,9 +297,10 @@ export const LadderKind = styled(Text, {
export const LadderHere = styled(Text, {
fontSize: "9.5px",
fontWeight: "$bold",
lineHeight: 1,
textTransform: "uppercase",
letterSpacing: "0.06em",
padding: "2px 8px",
padding: "3px 8px",
borderRadius: "$pill",
whiteSpace: "nowrap",
variants: {
Expand Down
39 changes: 19 additions & 20 deletions components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Link, Text } from "@obolnetwork/obol-ui";
import { Box, Text } from "@obolnetwork/obol-ui";
import NextLink from "next/link";
import { useRouter } from "next/router";
import { SITE_NAME } from "@constants/index";
Expand All @@ -25,32 +25,31 @@ export function Navbar() {
backgroundColor: "$bg01",
}}
>
<NextLink href="/" passHref legacyBehavior>
<Link css={{ textDecoration: "none" }}>
<Text variant="h4" css={{ color: "$body", fontWeight: "$semibold" }}>
{SITE_NAME}
</Text>
</Link>
</NextLink>
<Box as={NextLink} href="/" css={{ textDecoration: "none" }}>
<Text variant="h4" css={{ color: "$body", fontWeight: "$semibold" }}>
{SITE_NAME}
</Text>
</Box>
<Box as="nav" css={{ display: "flex", gap: "$lg" }}>
{NAV_ITEMS.map(({ href, label }) => {
const active =
href === "/"
? router.pathname === "/"
: router.pathname.startsWith(href.replace(/\/$/, ""));
return (
<NextLink key={href} href={href} passHref legacyBehavior>
<Link
css={{
color: active ? "var(--theme-brand)" : "$body",
fontWeight: active ? "$semibold" : "$normal",
textDecoration: "none",
"&:hover": { color: "var(--theme-brand)" },
}}
>
{label}
</Link>
</NextLink>
<Box
as={NextLink}
key={href}
href={href}
css={{
color: active ? "var(--theme-brand)" : "$body",
fontWeight: active ? "$semibold" : "$normal",
textDecoration: "none",
"&:hover": { color: "var(--theme-brand)" },
}}
>
{label}
</Box>
);
})}
</Box>
Expand Down
38 changes: 25 additions & 13 deletions components/pizza/Pizza.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { SLICES } from "@lib/rubric";
import type { Answers, SliceId, Stage } from "@lib/rubric/types";
import {
PIZZA_EMPTY,
PIZZA_EMPTY_STROKE,
PIZZA_FILL,
PIZZA_INK,
PIZZA_PLATE,
PIZZA_RING,
} from "@lib/theme/tokens";
import type { Answers, SliceColor, SliceId, Stage } from "@lib/rubric/types";

/**
* Theme-reactive colors. The in-app pizza reads CSS variables so it follows
* light/dark mode; the static OG share images use the hex constants in
* lib/theme/tokens.ts instead (they're pre-rendered at build time).
*/
const PIZZA_FILL: Record<SliceColor, string> = {
green: "var(--vb-green)",
yellow: "var(--vb-yellow)",
red: "var(--vb-red)",
};
const PIZZA_RING: Record<SliceColor, string> = {
green: "var(--theme-risk-green-ring)",
yellow: "var(--theme-risk-yellow-ring)",
red: "var(--theme-risk-red-ring)",
};
const PIZZA_PLATE = "var(--vb-plate)";
const PIZZA_EMPTY = "var(--vb-empty)";
const PIZZA_EMPTY_STROKE = "var(--vb-empty-stroke)";
const PIZZA_INK = "var(--vb-ink)";

const RAD = Math.PI / 180;

Expand Down Expand Up @@ -185,8 +197,8 @@ export function Pizza({
)}
{showLabels && (
<text
x={lx}
y={ly}
x={+lx.toFixed(2)}
y={+ly.toFixed(2)}
className="vbpizza__lbl"
textAnchor="middle"
dominantBaseline="middle"
Expand Down Expand Up @@ -235,7 +247,7 @@ export function Pizza({
className="vbpizza__progress"
style={{
fill: "none",
stroke: ringTone || "#9cc2c9",
stroke: ringTone || "var(--ice)",
strokeWidth: 3.5,
strokeLinecap: frac >= 1 ? "butt" : "round",
strokeDasharray:
Expand All @@ -257,7 +269,7 @@ export function Pizza({
y={cy - size * 0.028}
className="vbpizza__stagek"
textAnchor="middle"
style={{ fontSize: size * 0.033 }}
style={{ fill: PIZZA_INK, fontSize: size * 0.033 }}
>
STAGE
</text>
Expand Down
3 changes: 3 additions & 0 deletions hooks/useAssessment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function useAssessment() {

const start = useCallback(() => setStarted(true), []);
const back = useCallback(() => setStep((s) => Math.max(s - 1, 0)), []);
/** Return to the intro screen, keeping answers and step so progress resumes. */
const toIntro = useCallback(() => setStarted(false), []);
const goto = useCallback((i: number) => {
setStarted(true);
setStep(Math.max(0, Math.min(i, total)));
Expand Down Expand Up @@ -87,6 +89,7 @@ export function useAssessment() {
total,
choose,
back,
toIntro,
goto,
showResults,
takeResultsConfetti,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@stitches/react": "1.2.8",
"html-to-image": "1.11.13",
"next": "15.5.15",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-is": "19.2.3"
Expand Down
18 changes: 13 additions & 5 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppThemeProvider } from "@components/AppThemeProvider";
import { globalCss, Provider as TooltipProvider } from "@obolnetwork/obol-ui";
import "@styles/colors_and_type.css";
import "@styles/obol-bridge.css";
Expand Down Expand Up @@ -48,8 +49,14 @@ export default function App({ Component, pageProps }: AppProps) {
: undefined;

return (
<TooltipProvider>
<Head>
<AppThemeProvider
attribute="data-theme"
defaultTheme="light"
storageKey="vb-theme"
enableSystem={false}
>
<TooltipProvider>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
Expand All @@ -65,8 +72,9 @@ export default function App({ Component, pageProps }: AppProps) {
<meta name="twitter:image" content={ogImage} />
</>
)}
</Head>
<Component {...pageProps} />
</TooltipProvider>
</Head>
<Component {...pageProps} />
</TooltipProvider>
</AppThemeProvider>
);
}
2 changes: 1 addition & 1 deletion pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";

export default function Document() {
return (
<Html lang="en">
<Html lang="en" suppressHydrationWarning>
<Head>
<style id="obol" dangerouslySetInnerHTML={{ __html: getCssText() }} />
<link rel="icon" type="image/svg+xml" href={`${BASE_PATH}/icon.svg`} />
Expand Down
Loading
Loading