Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | 4x 4x 16x 39x 39x 16x 16x 22x 2x 4x 5x 10x 33x | "use client";
import type { ReactNode } from "react";
import Image from "next/image";
import { Button } from "@/components/ui";
import { TierBadge } from "@/components/ui/TierBadge";
import {
buildStanding,
type CompareRoundResult,
type SampleItem,
type Tier,
type TriageOutcome,
} from "./sampleData";
interface WelcomeStandingProps {
comparisons: CompareRoundResult[];
triage: Record<string, TriageOutcome>;
onNext: () => void;
}
const HEADING_ID = "welcome-standing-heading";
// Beat 4 — the payoff, mirroring the real results page (RankedResults): the
// beat is titled "Your ranking so far", the most-loved item leads under the
// "What you love most" eyebrow, then the ranking, then the onward / set-aside /
// deferred groups in subordinate styling, closing on the affirmation. The
// ranking is built from the user's own taps (buildStanding), so the top feels
// earned — the anti-marketplace weight: from things you own.
export function WelcomeStanding({ comparisons, triage, onNext }: WelcomeStandingProps) {
const standing = buildStanding(comparisons, triage);
const loved = standing.ranked.filter((row) => row.tier !== "onward");
const onward = standing.ranked.filter((row) => row.tier === "onward");
const [hero, ...lovedRest] = loved;
return (
<section aria-labelledby={HEADING_ID} className="flex flex-col items-center text-center gap-6">
<h1 id={HEADING_ID} className="font-display text-3xl sm:text-4xl text-ink">
Your ranking so far
</h1>
<p className="max-w-md text-ink-soft">
Your pair choices put the ones you love most in order.
</p>
{hero ? (
<div className="flex flex-col items-center gap-2">
<p className="text-sm font-semibold uppercase tracking-wide text-ink-soft">
What you love most
</p>
<div className="relative w-48 h-48 rounded-card overflow-hidden bg-content-soft shadow-card">
<Image src={hero.item.imageSrc} alt={hero.item.name} fill className="object-cover" unoptimized />
</div>
<p className="font-display text-xl text-ink">{hero.item.name}</p>
<TierBadge tier={hero.tier} />
</div>
) : (
<p className="max-w-sm text-ink-soft">
Nothing needs to rise yet. The pieces you stepped around are waiting below.
</p>
)}
{lovedRest.length > 0 && (
<ol className="flex flex-col gap-2 w-full max-w-sm">
{lovedRest.map((row, index) => (
<StandingRowItem
key={row.item.id}
item={row.item}
tier={row.tier}
rank={index + 2}
/>
))}
</ol>
)}
{onward.length > 0 && (
<StandingGroup
label="Where these go next"
caption="These are ready to move on — to a new home, or simply on from here."
>
{onward.map((row) => (
<StandingRowItem key={row.item.id} item={row.item} tier={row.tier} />
))}
</StandingGroup>
)}
{standing.setAside.length > 0 && (
<StandingGroup label="Set aside — waiting, whenever you want them">
{standing.setAside.map((item) => (
<StandingRowItem key={item.id} item={item} />
))}
</StandingGroup>
)}
{/* A Not yet piece rests in Still deciding, as on the real Ranking. */}
{standing.deferred.length > 0 && (
<StandingGroup label="Still deciding">
{standing.deferred.map((item) => (
<StandingRowItem key={item.id} item={item} />
))}
</StandingGroup>
)}
{/* Affirmation is always the last content beat (mirrors RankedResults):
re-anchor to the loved top, never the letting-go groups above it. */}
{hero && <p className="font-display text-xl text-ink">What you love most is at the top.</p>}
<Button size="lg" onClick={onNext}>
Continue
</Button>
</section>
);
}
// A quiet, subordinate group (onward / set-aside / deferred): a text-xl
// ink-soft eyebrow heading over its rows, mirroring RankedResults' Onward
// section, so these read as secondary to the loved ranking above.
function StandingGroup({
label,
caption,
children,
}: {
label: string;
caption?: string;
children: ReactNode;
}) {
return (
<section className="flex flex-col items-center gap-2 w-full">
<h3 className="font-display text-xl text-ink-soft">{label}</h3>
{caption && <p className="text-sm text-ink-soft">{caption}</p>}
<ol className="flex flex-col gap-2 w-full max-w-sm">{children}</ol>
</section>
);
}
// One standing row: image + name, with a tier badge only when the row carries a
// tier (loved/onward rows do; set-aside and deferred items don't).
function StandingRowItem({
item,
tier,
rank,
}: {
item: SampleItem;
tier?: Tier;
rank?: number;
}) {
return (
<li className="flex items-center gap-3 rounded-card p-2 bg-glass border border-rim">
{rank && (
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-content-soft text-sm font-semibold text-ink">
{rank}
</span>
)}
<span className="relative w-12 h-12 rounded-control overflow-hidden bg-content-soft shrink-0">
<Image src={item.imageSrc} alt={item.name} fill className="object-cover" unoptimized />
</span>
<span className="flex-1 text-left text-ink">{item.name}</span>
{tier && <TierBadge tier={tier} showLabel={false} />}
</li>
);
}
|