Skip to content

Commit b884512

Browse files
feat(marketing): tier icons and factory backdrop for leaderboard pages (#6889)
* feat(marketing): tier icons and factory backdrop for leaderboard, stats and profiles Adds a shared TierIcon: pixel-art gauges drawn on a 9x9 grid and rendered as rect runs with crispEdges, so the same art stays sharp at 9px in a table badge and 36px in the hero. Fill level encodes the tier, and the art inherits currentColor so the tier palette drives it with no per-icon colour. Adds FactoryBackdrop, parameterised by tint and container half-width: factory floor grid fading down the page, a furnace glow at the top, container guide lines and a brand hairline. Leaderboard and stats use the brand tint; profile pages tint by the developer's own tier and pick up a matching avatar crest. Equalises the tier palette against the stat numbers. brand-light sits at relative luminance 0.331, but emerald and amber were reading at 0.47 and 0.45, which made the tier strip look uneven. All four tiers now land at ~0.33 and 7:1 against the background. Unreached tiers keep their own icon and hue at 38% alpha rather than collapsing to grey, so the ladder is legible before it is climbed. Claude-Session: https://claude.ai/code/session_019DCP9fGeArbr5w13kQ14hR * fix(marketing): make the tier gauge legible at ship sizes, document the API CDP screenshots of the real pages showed the gauge's corner brackets collapsing into 2px stubs at 27px, reading as scattered fragments rather than a gauge. Worst on the profile avatar crest, where it looked like a rendering artifact. The frame is now continuous so the icon reads as a vessel filling at every size it ships at. Adds docstrings to the exported surface of TierIcon, TierBadge and FactoryBackdrop. Claude-Session: https://claude.ai/code/session_019DCP9fGeArbr5w13kQ14hR * refactor(marketing): drop docstrings from the tier and backdrop components Claude-Session: https://claude.ai/code/session_019DCP9fGeArbr5w13kQ14hR
1 parent b8318f7 commit b884512

10 files changed

Lines changed: 258 additions & 35 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const FLOOR_GRID =
2+
"linear-gradient(to right, rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.04) 1px, transparent 1px)";
3+
4+
const FLOOR_FADE = "linear-gradient(to bottom, black, transparent 62%)";
5+
6+
const BRAND_RGB = "210,86,17";
7+
8+
const guideLines = (halfWidth: number) =>
9+
`linear-gradient(to right, transparent calc(50% - ${halfWidth}px), rgba(255,255,255,0.07) calc(50% - ${halfWidth}px), rgba(255,255,255,0.07) calc(50% - ${halfWidth - 1}px), transparent calc(50% - ${halfWidth - 1}px), transparent calc(50% + ${halfWidth - 1}px), rgba(255,255,255,0.07) calc(50% + ${halfWidth - 1}px), rgba(255,255,255,0.07) calc(50% + ${halfWidth}px), transparent calc(50% + ${halfWidth}px))`;
10+
11+
const furnaceGlow = (rgb: string) =>
12+
`radial-gradient(ellipse 62% 100% at 50% 0%, rgba(${rgb},0.14), rgba(${rgb},0.035) 45%, transparent 72%)`;
13+
14+
interface FactoryBackdropProps {
15+
tint?: string;
16+
halfWidth?: number;
17+
}
18+
19+
export function FactoryBackdrop({
20+
tint = BRAND_RGB,
21+
halfWidth = 448,
22+
}: FactoryBackdropProps) {
23+
return (
24+
<div
25+
aria-hidden="true"
26+
className="pointer-events-none absolute inset-0 overflow-hidden"
27+
>
28+
<div
29+
className="absolute inset-0"
30+
style={{
31+
backgroundImage: FLOOR_GRID,
32+
backgroundSize: "32px 32px",
33+
maskImage: FLOOR_FADE,
34+
WebkitMaskImage: FLOOR_FADE,
35+
}}
36+
/>
37+
<div
38+
className="absolute inset-x-0 top-0 h-[620px]"
39+
style={{ backgroundImage: furnaceGlow(tint) }}
40+
/>
41+
<div
42+
className="absolute inset-0"
43+
style={{ backgroundImage: guideLines(halfWidth) }}
44+
/>
45+
<div
46+
className="absolute inset-x-0 top-0 h-px"
47+
style={{
48+
backgroundImage: `linear-gradient(to right, transparent, rgba(${tint},0.55), transparent)`,
49+
}}
50+
/>
51+
</div>
52+
);
53+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { FactoryBackdrop } from "./FactoryBackdrop";

apps/marketing/src/app/components/TierBadge/TierBadge.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1+
import { TierIcon } from "@/app/components/TierIcon";
2+
13
export const TIER_NAMES = [
24
"Button pusher",
35
"Operator",
46
"Plant Manager",
57
"Henry Ford",
68
] as const;
79

8-
const TIER_STYLES = [
9-
"text-muted-foreground border-border",
10-
"text-sky-400 border-sky-400/30",
11-
"text-emerald-400 border-emerald-400/30",
12-
"text-brand border-brand/40",
10+
export const TIER_RGB = [
11+
"147,157,171",
12+
"91,157,251",
13+
"49,176,108",
14+
"219,135,34",
1315
] as const;
1416

17+
const LOCKED_RGB = "255,255,255";
18+
19+
export const tierRgb = (tier: number): string =>
20+
TIER_RGB[tier - 1] ?? TIER_RGB[1];
21+
1522
export const tierLabel = (tier: number): string =>
1623
tier >= 1 && tier <= 4 ? (TIER_NAMES[tier - 1] ?? "Unranked") : "Unranked";
1724

@@ -29,14 +36,22 @@ export function TierBadge({
2936
}: TierBadgeProps) {
3037
const ranked = tier >= 1 && tier <= 4;
3138
const style = ranked
32-
? (TIER_STYLES[tier - 1] ?? TIER_STYLES[0])
33-
: "text-muted-foreground/60 border-border/60";
39+
? {
40+
color: `rgb(${tierRgb(tier)})`,
41+
borderColor: `rgba(${tierRgb(tier)},0.4)`,
42+
}
43+
: {
44+
color: `rgba(${LOCKED_RGB},0.4)`,
45+
borderColor: `rgba(${LOCKED_RGB},0.14)`,
46+
};
3447

3548
if (size === "hero") {
3649
return (
3750
<div
38-
className={`inline-flex flex-col items-center border px-8 py-4 ${style} ${className}`}
51+
className={`inline-flex flex-col items-center border px-8 py-4 ${className}`}
52+
style={style}
3953
>
54+
<TierIcon tier={tier} size={36} hollow={!ranked} className="mb-3" />
4055
<span className="font-mono text-[0.58rem] uppercase tracking-[0.2em] opacity-60">
4156
{ranked ? `Factory tier ${tier}` : "Unranked"}
4257
</span>
@@ -49,8 +64,10 @@ export function TierBadge({
4964

5065
return (
5166
<span
52-
className={`inline-block border px-2 py-0.5 font-mono text-[0.62rem] uppercase tracking-[0.1em] whitespace-nowrap ${style} ${className}`}
67+
className={`inline-flex items-center gap-1.5 border px-2 py-0.5 font-mono text-[0.62rem] uppercase tracking-[0.1em] whitespace-nowrap ${className}`}
68+
style={style}
5369
>
70+
<TierIcon tier={tier} size={9} hollow={!ranked} />
5471
{tierLabel(tier)}
5572
</span>
5673
);
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
export { TIER_NAMES, TierBadge, tierLabel } from "./TierBadge";
1+
export {
2+
TIER_NAMES,
3+
TIER_RGB,
4+
TierBadge,
5+
tierLabel,
6+
tierRgb,
7+
} from "./TierBadge";
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const GRID = 9;
2+
3+
const ART = [
4+
[
5+
"#########",
6+
"#.......#",
7+
"#.......#",
8+
"#.......#",
9+
"#.......#",
10+
"#.......#",
11+
"#.......#",
12+
"#.#####.#",
13+
"#########",
14+
],
15+
[
16+
"#########",
17+
"#.......#",
18+
"#.......#",
19+
"#.......#",
20+
"#.......#",
21+
"#.......#",
22+
"#.#####.#",
23+
"#.#####.#",
24+
"#########",
25+
],
26+
[
27+
"#########",
28+
"#.......#",
29+
"#.......#",
30+
"#.......#",
31+
"#.......#",
32+
"#.#####.#",
33+
"#.#####.#",
34+
"#.#####.#",
35+
"#########",
36+
],
37+
[
38+
"#########",
39+
"#.......#",
40+
"#.#####.#",
41+
"#.#####.#",
42+
"#.#####.#",
43+
"#.#####.#",
44+
"#.#####.#",
45+
"#.#####.#",
46+
"#########",
47+
],
48+
] as const;
49+
50+
const FRAME = [
51+
"#########",
52+
"#.......#",
53+
"#.......#",
54+
"#.......#",
55+
"#.......#",
56+
"#.......#",
57+
"#.......#",
58+
"#.......#",
59+
"#########",
60+
] as const;
61+
62+
function rowRuns(row: string): Array<[number, number]> {
63+
const runs: Array<[number, number]> = [];
64+
let start = -1;
65+
for (let x = 0; x <= row.length; x++) {
66+
if (row[x] === "#") {
67+
if (start < 0) start = x;
68+
} else if (start >= 0) {
69+
runs.push([start, x - start]);
70+
start = -1;
71+
}
72+
}
73+
return runs;
74+
}
75+
76+
interface TierIconProps {
77+
tier: number;
78+
79+
size?: number;
80+
hollow?: boolean;
81+
className?: string;
82+
}
83+
84+
export function TierIcon({
85+
tier,
86+
size = 18,
87+
hollow = false,
88+
className = "",
89+
}: TierIconProps) {
90+
const art = hollow ? FRAME : ART[tier - 1];
91+
if (!art) return null;
92+
93+
return (
94+
<svg
95+
aria-hidden="true"
96+
width={size}
97+
height={size}
98+
viewBox={`0 0 ${GRID} ${GRID}`}
99+
shapeRendering="crispEdges"
100+
className={`shrink-0 ${className}`}
101+
>
102+
{art.flatMap((row, y) =>
103+
rowRuns(row).map(([x, width]) => (
104+
<rect
105+
key={`${y}:${x}`}
106+
x={x}
107+
y={y}
108+
width={width}
109+
height={1}
110+
fill="currentColor"
111+
/>
112+
)),
113+
)}
114+
</svg>
115+
);
116+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { TierIcon } from "./TierIcon";

apps/marketing/src/app/components/TierTube/TierTube.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { TIER_NAMES } from "@/app/components/TierBadge";
1+
import { TIER_NAMES, TIER_RGB } from "@/app/components/TierBadge";
2+
import { TierIcon } from "@/app/components/TierIcon";
23

3-
const ZONES = [
4-
{ tier: 1, rgb: "138,144,153" },
5-
{ tier: 2, rgb: "91,157,251" },
6-
{ tier: 3, rgb: "63,207,127" },
7-
{ tier: 4, rgb: "242,163,60" },
8-
] as const;
4+
const ZONES = TIER_RGB.map((rgb, index) => ({ tier: index + 1, rgb }));
95

106
const railLeft = (position: number) =>
117
position <= 0 ? 0 : ((position - 0.5) / ZONES.length) * 100;
@@ -121,9 +117,7 @@ export function TierTube({
121117
isActive ? "animate-station-pulse" : ""
122118
}`}
123119
style={{
124-
borderColor: reached
125-
? `rgb(${rgb})`
126-
: "rgba(255,255,255,0.16)",
120+
borderColor: reached ? `rgb(${rgb})` : `rgba(${rgb},0.4)`,
127121
background: reached ? `rgb(${rgb})` : "transparent",
128122
boxShadow: isActive
129123
? `0 0 0 4px rgba(${rgb},0.16)`
@@ -160,15 +154,19 @@ export function TierTube({
160154
/>
161155
)}
162156
<div
163-
className={`text-xl leading-none ${
164-
pixelClassName || "font-mono tracking-tight"
165-
}`}
157+
className="flex items-center gap-3"
166158
style={{
167-
color: reached ? `rgb(${rgb})` : undefined,
168-
opacity: reached ? 1 : 0.35,
159+
color: reached ? `rgb(${rgb})` : `rgba(${rgb},0.38)`,
169160
}}
170161
>
171-
{value}
162+
<TierIcon tier={tier} size={27} />
163+
<span
164+
className={`text-xl leading-none ${
165+
pixelClassName || "font-mono tracking-tight"
166+
}`}
167+
>
168+
{value}
169+
</span>
172170
</div>
173171
<div className="font-mono text-[0.65rem] uppercase tracking-[0.12em] text-muted-foreground mt-2">
174172
{TIER_NAMES[tier - 1]}

apps/marketing/src/app/leaderboard/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { COMPANY } from "@superset/shared/constants";
22
import type { Metadata } from "next";
33
import { Silkscreen } from "next/font/google";
44
import Link from "next/link";
5+
import { FactoryBackdrop } from "@/app/components/FactoryBackdrop";
56
import { fetchStandings, fetchStats } from "@/app/utils/fetchLeaderboard";
67
import { LeaderboardBoard } from "./components/LeaderboardBoard";
78

@@ -45,6 +46,8 @@ export default async function LeaderboardPage() {
4546

4647
return (
4748
<main className="relative min-h-screen">
49+
<FactoryBackdrop />
50+
4851
<div className="relative max-w-4xl mx-auto px-6 py-10 md:py-14">
4952
<header className="text-center pt-6 md:pt-10">
5053
<h1

apps/marketing/src/app/stats/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { COMPANY } from "@superset/shared/constants";
22
import type { Metadata } from "next";
33
import { Silkscreen } from "next/font/google";
44
import Link from "next/link";
5+
import { FactoryBackdrop } from "@/app/components/FactoryBackdrop";
56
import { fetchStats } from "@/app/utils/fetchLeaderboard";
67
import { formatDayRange } from "@/app/utils/formatUsage";
78
import { StatsBody } from "./components/StatsBody";
@@ -42,7 +43,9 @@ export default async function StatsPage() {
4243

4344
return (
4445
<main className="relative min-h-screen">
45-
<div className="max-w-4xl mx-auto px-6 py-10 md:py-14">
46+
<FactoryBackdrop />
47+
48+
<div className="relative max-w-4xl mx-auto px-6 py-10 md:py-14">
4649
<header className="text-center pt-6 md:pt-10">
4750
<h1
4851
className={`${pixel.className} text-3xl md:text-4xl text-foreground`}

0 commit comments

Comments
 (0)