Skip to content
Merged
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
8 changes: 3 additions & 5 deletions components/Onboarding/MeasuringCatalogPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { MEASURING_BODY, MEASURING_TITLE } from "@/lib/catalog/measuringCopy";

/**
* Terminal state for `/setup/valuation` while a catalog exists but has no
Expand All @@ -15,12 +16,9 @@ import { cn } from "@/lib/utils";
*/
const MeasuringCatalogPanel = () => (
<section className="mx-auto flex w-full max-w-xl flex-col items-center gap-4 px-6 py-8 text-center">
<h1 className="text-2xl font-semibold text-foreground">
Measuring your catalog
</h1>
<h1 className="text-2xl font-semibold text-foreground">{MEASURING_TITLE}</h1>
<p className="text-sm text-muted-foreground">
We are pulling play counts for every track. This usually takes a minute.
Your baseline value appears here as soon as it is ready.
{MEASURING_BODY} Your baseline value appears here as soon as it is ready.
</p>
<Link href="/setup/tasks" className={cn(buttonVariants(), "min-w-[200px]")}>
Set up your weekly report
Expand Down
12 changes: 8 additions & 4 deletions hooks/useAddSpotifyArtist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { addSpotifyArtist } from "@/lib/artists/addSpotifyArtist";
import { runValuation } from "@/lib/valuation/runValuation";
import {
MEASURING_TOAST_ERROR,
MEASURING_TOAST_SUCCESS,
measuringToastLoading,
} from "@/lib/catalog/measuringCopy";
import { useArtistProvider } from "@/providers/ArtistProvider";
import { useOrganization } from "@/providers/OrganizationProvider";
import useCatalogs from "@/hooks/useCatalogs";
Expand Down Expand Up @@ -56,10 +61,9 @@ export function useAddSpotifyArtist() {
queryClient.invalidateQueries({ queryKey: ["catalogs"] });
}),
{
loading: `Valuing ${artist.name}'s catalog…`,
success: "Your catalog is ready",
error:
"Couldn't value the catalog automatically — you can claim it later",
loading: measuringToastLoading(artist.name),
success: MEASURING_TOAST_SUCCESS,
error: MEASURING_TOAST_ERROR,
},
);
}
Expand Down
57 changes: 57 additions & 0 deletions lib/catalog/__tests__/measuringCopy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import {
MEASURING_TITLE,
MEASURING_ESTIMATE,
MEASURING_BODY,
MEASURING_TOAST_SUCCESS,
MEASURING_TOAST_ERROR,
measuringToastLoading,
} from "@/lib/catalog/measuringCopy";
import { getCatalogReportEmptyCopy } from "@/lib/catalog/getCatalogReportEmptyCopy";

describe("measuringCopy", () => {
// chat#1912 row 10: the same minute of waiting was described four different
// ways across the seeding toast, /setup/valuation, /catalogs/{id} and
// marketing, with three different time estimates. The estimate a customer
// gets should not depend on which surface they happen to be looking at.
it("states the time estimate once", () => {
expect(MEASURING_ESTIMATE).toContain("about a minute");
expect(MEASURING_BODY).toContain(MEASURING_ESTIMATE);
});

it("is used by the catalog report's measuring state", () => {
const copy = getCatalogReportEmptyCopy("measuring", { hasOwnCatalogs: true });

expect(copy.title).toBe(MEASURING_TITLE);
expect(copy.body).toContain(MEASURING_ESTIMATE);
});

it("keeps the report's own reassurance without restating the estimate", () => {
const copy = getCatalogReportEmptyCopy("measuring", { hasOwnCatalogs: true });

expect(copy.body).toContain("no need to run the valuation again");
// The estimate must appear once in the sentence, not twice.
expect(copy.body.split("about a minute").length - 1).toBe(1);
});

// Derived from the title rather than hard-coded, so the assertion fails if
// either side drifts — the point is that they agree, not that they are any
// particular word.
it("uses the same verb in the seeding toast as on the pages", () => {
const verb = MEASURING_TITLE.split(" ")[0];

expect(verb).toBe("Measuring");
expect(measuringToastLoading("BennyJ504")).toBe(`${verb} BennyJ504's catalog…`);
});

it("states the toast outcomes", () => {
expect(MEASURING_TOAST_SUCCESS).toBe("Your catalog is ready");
expect(MEASURING_TOAST_ERROR).toContain("claim it later");
});

// House style: em dashes read as machine-written in product copy.
it("keeps the shared copy free of em dashes", () => {
const all = [MEASURING_TITLE, MEASURING_ESTIMATE, MEASURING_BODY, MEASURING_TOAST_SUCCESS, MEASURING_TOAST_ERROR].join(" ");
expect(all).not.toMatch(/[—–]/);
});
});
5 changes: 3 additions & 2 deletions lib/catalog/getCatalogReportEmptyCopy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CatalogReportState } from "./getCatalogReportState";
import { MEASURING_BODY, MEASURING_TITLE } from "./measuringCopy";

export type CatalogReportEmptyState = Exclude<
CatalogReportState,
Expand All @@ -24,8 +25,8 @@ const COPY: Record<CatalogReportEmptyState, CatalogReportEmptyCopy> = {
cta: { label: "Sign in", action: "login" },
},
measuring: {
title: "Measuring your catalog",
body: "We are pulling live play counts for every track. This usually takes about a minute, and the report appears here on its own. There is no need to run the valuation again.",
title: MEASURING_TITLE,
body: `${MEASURING_BODY} The report appears here on its own, so there is no need to run the valuation again.`,
},
"other-account": {
title: "This catalog was measured by another account",
Expand Down
30 changes: 30 additions & 0 deletions lib/catalog/measuringCopy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* One description of one wait.
*
* The minute between seeding a catalog and its measurements landing is shown on
* three chat surfaces — the `/setup/artists` seeding toast, `/setup/valuation`,
* and the `/catalogs/{id}` report — and each had drifted into its own wording
* and its own time estimate ("a minute", "about a minute", "a minute or two").
* The estimate a customer is given should not depend on which surface they
* happen to be looking at (chat#1912 row 10).
*
* Marketing keeps its own copy of these strings, since it is a separate
* deployment; the contract is that the text matches, not that the module is
* imported across repos.
*/
export const MEASURING_TITLE = "Measuring your catalog";

/** The single source for how long this takes. Change it here, nowhere else. */
export const MEASURING_ESTIMATE =
"This usually takes about a minute, and longer for large catalogs.";

export const MEASURING_BODY = `We are pulling live play counts for every track. ${MEASURING_ESTIMATE}`;

/** Seeding runs in the background, so its toast uses the same verb as the pages. */
export const measuringToastLoading = (artistName: string) =>
`Measuring ${artistName}'s catalog…`;

export const MEASURING_TOAST_SUCCESS = "Your catalog is ready";

export const MEASURING_TOAST_ERROR =
"Couldn't measure the catalog automatically, you can claim it later";
Loading