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 | 23x 28x 12x | import type { Item } from "@/types";
import type { UsageAnswer } from "@/lib/derive-usage";
import { ItemCard } from "@/features/items/ItemCard";
import { MostLovedHero } from "@/features/results/MostLovedHero";
import { effectiveBucket, type Bucket } from "@/lib/results-buckets";
/** The minimal per-row shape both the live Standing and a finalized snapshot
* share: a stable key, the item id, and the two bucket fields effectiveBucket
* reads. Live preferences carry no manual overlay, so manualBucket is null. */
export interface RankedRow {
id: string;
itemId: string;
computedBucket: Bucket;
manualBucket: Bucket | null;
}
export interface RankedJoinedRow {
ri: RankedRow;
item: Item;
}
interface RankedResultsProps {
/** The loved tail (top/high/medium), most-loved first. loved[0] is the hero. */
loved: RankedJoinedRow[];
/** The Onward tail (low), asked forward — never tallied. */
onward: RankedJoinedRow[];
collectionId: string;
coverUrl: (itemId: string) => string | undefined;
/** Epic S1: the live usage fold, keyed by itemId. A row whose item is in the
* map gets a quiet usage chip; an absent item gets none (undecided). The raw
* event log is NEVER passed here — only this small derived map (DATA-6). */
usageByItemId?: ReadonlyMap<string, UsageAnswer>;
}
/** The affirmation composition for a saved ranking (`/collections/[id]/results/[resultNumber]`):
* most-loved hero → the full ranking → the forward-framed Onward section → an affirmation close.
* The caller derives loved/onward from the partitionRanked/effectiveBucket source (TONE #1
* affirmation-first, no counts). The welcome walkthrough's `WelcomeStanding` mirrors this shape
* with its own rows. */
export function RankedResults({ loved, onward, collectionId, coverUrl, usageByItemId }: RankedResultsProps) {
return (
<div className="space-y-8">
{loved.length > 0 && (
<MostLovedHero
item={loved[0].item}
collectionId={collectionId}
heroPhotoUrl={coverUrl(loved[0].item.id)}
/>
)}
{loved.length > 1 && (
<section className="space-y-4">
<h2 className="font-display text-2xl text-ink">The full ranking</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{loved.slice(1).map(({ ri, item }) => (
<ItemCard
key={ri.id}
item={item}
collectionId={collectionId}
heroPhotoUrl={coverUrl(item.id)}
bucket={effectiveBucket(ri)}
usage={usageByItemId?.get(item.id)}
/>
))}
</div>
</section>
)}
{/* All-Onward: no loved set, no hero. Lead from the person's side —
honor the deciding and the clarity reached, never absence-of-love,
never a count (TONE #1 affirmation-not-subtraction, #4 unpressured). */}
{loved.length === 0 && onward.length > 0 && (
<section className="space-y-3">
<h2 className="font-display text-3xl text-ink">You’ve made your choices.</h2>
<p className="text-lg text-ink">You sat with each one and let yourself feel clearly. That clarity is the gift here — and it’s yours.</p>
</section>
)}
{/* Onward: the lowest tier, asked forward — where these go next — never
tallied, never framed as discarding (TONE #1, #2). */}
{/* pt-2: the small eyebrow needs more air than the bare section gap — 32 + 8 = 40px, DESIGN.md
spacing xl (purses-w7ss.22). */}
{onward.length > 0 && (
<section className="space-y-4 pt-2">
<div className="space-y-1">
{/* Onward is subordinate to the loved ranking (TONE #1): a quieter
heading (text-xl vs the loved tail's text-2xl, ink-soft), with the
"Onward" eyebrow folded INTO the heading so heading-nav users hear
"Onward where these go next" as one accessible name. */}
<h2 className="font-display text-xl text-ink-soft">
<span className="block text-sm font-semibold uppercase tracking-wide">Onward</span>
<span>Where these go next</span>
</h2>
<p className="text-ink-soft">These are ready to move on — to a new home, or simply on from here.</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{onward.map(({ ri, item }) => (
<ItemCard
key={ri.id}
item={item}
collectionId={collectionId}
heroPhotoUrl={coverUrl(item.id)}
bucket={effectiveBucket(ri)}
usage={usageByItemId?.get(item.id)}
/>
))}
</div>
</section>
)}
{/* The last beat is affirmation, never the letting-go list (TONE #1).
When something was loved, re-anchor to the loved set at the TOP (the
hero/ranking), so the close never binds by proximity to the Onward pile
above it. When nothing was loved, the close cannot claim "love most"
(it would name the very items just sent Onward) — so all-Onward ends on
the person-side act of looking honestly. Either way the page ends on
affirmation, and this is always the composition's last content beat. */}
<p className="font-display text-xl text-ink">
{loved.length > 0
? "What you love most is at the top."
: "You looked, honestly, at every one. That took something."}
</p>
</div>
);
}
|