Skip to content
Open
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
29 changes: 18 additions & 11 deletions components/valuation/MeasuredCatalog.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -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 (
<div className="mt-9">
<p className="text-[11px] font-pixel uppercase tracking-[0.16em] text-(--foreground)/45">
Expand All @@ -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 => (
<MeasuredAlbumRow
key={album.id}
album={album}
meta={props.catalogAlbums.find(a => a.id === album.id)}
centralValue={props.centralValue}
totalStreams={props.totalStreams}
/>
))}
{measured.map(album => (
<MeasuredAlbumRow
key={album.id}
album={album}
meta={props.catalogAlbums.find(a => a.id === album.id)}
centralValue={props.centralValue}
totalStreams={props.totalStreams}
/>
))}
<UnmeasuredReleasesRow
albums={unmeasured}
catalogAlbums={props.catalogAlbums}
/>
</ul>
</div>
);
Expand Down
47 changes: 47 additions & 0 deletions components/valuation/UnmeasuredReleasesRow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<li className="bg-(--foreground)/[0.02]">
<details>
<summary className="flex cursor-pointer list-none items-center gap-3.5 px-4 py-3 transition-colors duration-200 hover:bg-(--foreground)/[0.04] [&::-webkit-details-marker]:hidden">
<span className="min-w-0 flex-1 text-[13px] text-(--foreground)/50">
+{count} {count === 1 ? "release" : "releases"} with no measurable
streams
</span>
<span className="text-[11px] text-(--foreground)/35">
not counted in the estimate
</span>
</summary>
<ul className="pb-2">
{props.albums.map(album => {
const meta = props.catalogAlbums.find(a => a.id === album.id);
return (
<li key={album.id} className="flex items-center gap-3.5 py-1.5 pr-4 pl-4">
<span className="min-w-0 flex-1 truncate text-[13px] text-(--foreground)/50">
{meta?.name ?? album.id}
</span>
<span className="text-[11px] tabular-nums text-(--foreground)/35">
{meta?.releaseDate?.slice(0, 4)}
</span>
</li>
);
})}
</ul>
</details>
</li>
);
}
62 changes: 62 additions & 0 deletions lib/valuation/__tests__/partitionMeasuredAlbums.test.ts
Original file line number Diff line number Diff line change
@@ -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"]);
});
});
20 changes: 20 additions & 0 deletions lib/valuation/partitionMeasuredAlbums.ts
Original file line number Diff line number Diff line change
@@ -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 };
}