From 41a2d0acb60f50fa8667e36baa68f86dc38970e9 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Jul 2026 12:26:48 -0500 Subject: [PATCH] feat: collapse the zero-stream release tail on the valuation card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unmeasured releases rendered as a wall of "$0 · 0 streams" rows directly above the "Get the full report" CTA (18 rows for Del Water Gap), reading as failure at the conversion moment. Group them into one expandable row: "+N releases with no measurable streams". Measured releases keep their sorted display; the split is a pure, tested function. Part of recoupable/chat#1850. Co-Authored-By: Claude Fable 5 --- components/valuation/MeasuredCatalog.tsx | 29 +++++---- .../valuation/UnmeasuredReleasesRow.tsx | 47 ++++++++++++++ .../__tests__/partitionMeasuredAlbums.test.ts | 62 +++++++++++++++++++ lib/valuation/partitionMeasuredAlbums.ts | 20 ++++++ 4 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 components/valuation/UnmeasuredReleasesRow.tsx create mode 100644 lib/valuation/__tests__/partitionMeasuredAlbums.test.ts create mode 100644 lib/valuation/partitionMeasuredAlbums.ts diff --git a/components/valuation/MeasuredCatalog.tsx b/components/valuation/MeasuredCatalog.tsx index a2b984a..8195d4e 100644 --- a/components/valuation/MeasuredCatalog.tsx +++ b/components/valuation/MeasuredCatalog.tsx @@ -1,5 +1,7 @@ import type { MeasuredAlbum, StartedAlbum } from "@/components/valuation/types"; import { MeasuredAlbumRow } from "@/components/valuation/MeasuredAlbumRow"; +import { UnmeasuredReleasesRow } from "@/components/valuation/UnmeasuredReleasesRow"; +import { partitionMeasuredAlbums } from "@/lib/valuation/partitionMeasuredAlbums"; type MeasuredCatalogProps = { albums: MeasuredAlbum[]; @@ -10,9 +12,12 @@ type MeasuredCatalogProps = { /** * The "What we measured" breakdown: every captured release, biggest first. + * Zero-stream releases collapse into a single count row at the end so the + * list never trails off in "$0" rows right above the CTA. */ export function MeasuredCatalog(props: MeasuredCatalogProps) { if (props.albums.length === 0) return null; + const { measured, unmeasured } = partitionMeasuredAlbums(props.albums); return (

@@ -24,17 +29,19 @@ export function MeasuredCatalog(props: MeasuredCatalogProps) { boxShadow: "0 0 0 1px color-mix(in srgb, var(--foreground) 10%, transparent)", }} > - {[...props.albums] - .sort((a, b) => b.streams - a.streams) - .map(album => ( - a.id === album.id)} - centralValue={props.centralValue} - totalStreams={props.totalStreams} - /> - ))} + {measured.map(album => ( + a.id === album.id)} + centralValue={props.centralValue} + totalStreams={props.totalStreams} + /> + ))} +

); diff --git a/components/valuation/UnmeasuredReleasesRow.tsx b/components/valuation/UnmeasuredReleasesRow.tsx new file mode 100644 index 0000000..192f020 --- /dev/null +++ b/components/valuation/UnmeasuredReleasesRow.tsx @@ -0,0 +1,47 @@ +import type { MeasuredAlbum, StartedAlbum } from "@/components/valuation/types"; + +type UnmeasuredReleasesRowProps = { + albums: MeasuredAlbum[]; + catalogAlbums: StartedAlbum[]; +}; + +/** + * Collapses the zero-stream tail of the "What we measured" list into one row + * ("+N releases with no measurable streams"), expandable to the release names. + * Rendering each as its own "$0 · 0 streams" row read as failure right above + * the CTA (chat#1850). + */ +export function UnmeasuredReleasesRow(props: UnmeasuredReleasesRowProps) { + if (props.albums.length === 0) return null; + const count = props.albums.length; + return ( +
  • +
    + + + +{count} {count === 1 ? "release" : "releases"} with no measurable + streams + + + not counted in the estimate + + +
      + {props.albums.map(album => { + const meta = props.catalogAlbums.find(a => a.id === album.id); + return ( +
    • + + {meta?.name ?? album.id} + + + {meta?.releaseDate?.slice(0, 4)} + +
    • + ); + })} +
    +
    +
  • + ); +} diff --git a/lib/valuation/__tests__/partitionMeasuredAlbums.test.ts b/lib/valuation/__tests__/partitionMeasuredAlbums.test.ts new file mode 100644 index 0000000..7bbb82d --- /dev/null +++ b/lib/valuation/__tests__/partitionMeasuredAlbums.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { partitionMeasuredAlbums } from "@/lib/valuation/partitionMeasuredAlbums"; +import type { MeasuredAlbum } from "@/components/valuation/types"; + +const album = (id: string, streams: number): MeasuredAlbum => ({ + id, + streams, + tracks: [{ name: `${id}-t1`, streams }], +}); + +describe("partitionMeasuredAlbums", () => { + it("splits zero-stream releases out of the measured list", () => { + const { measured, unmeasured } = partitionMeasuredAlbums([ + album("a", 100), + album("b", 0), + album("c", 50), + album("d", 0), + ]); + expect(measured.map(a => a.id)).toEqual(["a", "c"]); + expect(unmeasured.map(a => a.id)).toEqual(["b", "d"]); + }); + + it("sorts measured releases by streams, biggest first", () => { + const { measured } = partitionMeasuredAlbums([ + album("small", 1), + album("big", 1_000_000), + album("mid", 500), + ]); + expect(measured.map(a => a.id)).toEqual(["big", "mid", "small"]); + }); + + it("returns no unmeasured releases when everything has streams", () => { + const { measured, unmeasured } = partitionMeasuredAlbums([ + album("a", 2), + album("b", 1), + ]); + expect(measured).toHaveLength(2); + expect(unmeasured).toEqual([]); + }); + + it("returns everything as unmeasured when nothing has streams", () => { + const { measured, unmeasured } = partitionMeasuredAlbums([ + album("a", 0), + album("b", 0), + ]); + expect(measured).toEqual([]); + expect(unmeasured).toHaveLength(2); + }); + + it("handles an empty catalog", () => { + expect(partitionMeasuredAlbums([])).toEqual({ + measured: [], + unmeasured: [], + }); + }); + + it("does not mutate the input array", () => { + const input = [album("low", 1), album("high", 2)]; + partitionMeasuredAlbums(input); + expect(input.map(a => a.id)).toEqual(["low", "high"]); + }); +}); diff --git a/lib/valuation/partitionMeasuredAlbums.ts b/lib/valuation/partitionMeasuredAlbums.ts new file mode 100644 index 0000000..1f22e57 --- /dev/null +++ b/lib/valuation/partitionMeasuredAlbums.ts @@ -0,0 +1,20 @@ +import type { MeasuredAlbum } from "@/components/valuation/types"; + +/** + * Splits a valuation's releases into the ones with measured streams (sorted + * biggest first, ready to render) and the zero-stream tail. A wall of + * "$0 · 0 streams" rows right above the CTA reads as failure, so the UI + * collapses the unmeasured group into a single count row instead. + */ +export function partitionMeasuredAlbums(albums: MeasuredAlbum[]): { + measured: MeasuredAlbum[]; + unmeasured: MeasuredAlbum[]; +} { + const measured: MeasuredAlbum[] = []; + const unmeasured: MeasuredAlbum[] = []; + for (const album of albums) { + (album.streams > 0 ? measured : unmeasured).push(album); + } + measured.sort((a, b) => b.streams - a.streams); + return { measured, unmeasured }; +}