All files / src/components/today FirstRun.tsx

100% Statements 4/4
100% Branches 4/4
100% Functions 2/2
100% Lines 4/4

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                                                                                2x     23x               19x   19x                                                                                              
import { Link } from "@tanstack/react-router";
import type { PublishReadiness } from "../../hooks/queries/usePublishReadiness";
import { MARKETPLACE_URL } from "../../lib/env";
import { Button } from "../ui/button";
import { Card } from "../ui/card";
import { Separator } from "../ui/separator";
import { buildPublishPhases, PhaseRail } from "./PhaseRail";
 
/**
 * Today, first run. Plan ยง1.3; `.context/mockups/today-first-run.html`.
 *
 * ONE BAND, ONE ACTION, AND AN HONEST STATEMENT OF WHAT IT UNLOCKS.
 *
 * What this replaces, exactly: four zeros, an empty list, a 0% checklist of
 * six chores, and an amber "pending review" banner whose only message was
 * "Contact admin for more details." Every one of those is gone.
 *
 * The banner is gone because **there is no review**. A pro who clears the bar
 * is published automatically, by the server, with no human in the loop
 * (`maybeAutoPublishPro`). The banner was not merely unhelpful โ€” it was false.
 *
 * WHY THE COPY IS ALLOWED TO PROMISE THAT
 * "goes live the moment it is saved" is only honest because the bar is
 * objective and the transition is automatic. The promise and the phase rail
 * under it therefore read from the same server response that gates the
 * publish, so the sentence and the state can never drift apart.
 *
 * `photoMinimum` COMES FROM THE SERVER, IN THE PROSE TOO
 * "One room with three photos" is not a sentence with a 3 in it โ€” it is
 * `photoMinimum` spelled out. Change the bar in
 * `apps/api/src/lib/publish-readiness.ts` and this paragraph changes with it.
 *
 * TYPE
 * Display MD (`clamp(1.375rem, 2.2vw, 1.75rem)` โ†’ `text-2xl sm:text-3xl`, the
 * nearest steps on the token scale) is the biggest ramp the shell allows, and
 * an empty state is one of the only two places it is allowed. Display LG and
 * XL stay out of the shell entirely; auth gets the rest.
 */
 
/** `three` reads better than `3` in prose; past five, print the figure. */
const SPELLED = ["zero", "one", "two", "three", "four", "five"] as const;
 
export function spellMinimum(count: number): string {
	return SPELLED[count] ?? String(count);
}
 
export interface FirstRunCardProps {
	readiness: PublishReadiness | null | undefined;
}
 
export function FirstRunCard({ readiness }: FirstRunCardProps) {
	const minimum = readiness?.photoMinimum;
 
	return (
		<Card className="max-w-3xl">
			<div className="px-6 pt-10 pb-7 sm:px-7">
				<h2 className="text-2xl font-bold tracking-tight text-foreground-default sm:text-3xl">
					Add your first project
				</h2>
 
				<p className="mt-3 max-w-[62ch] text-base text-foreground-muted">
					{minimum === undefined
						? "One room with a few photos is all it takes."
						: `One room with ${spellMinimum(minimum)} photos is all it takes.`}{" "}
					Your studio goes live on the marketplace, your own website and search{" "}
					<strong className="font-semibold text-foreground-default">
						the moment it is saved
					</strong>{" "}
					&mdash; no waiting, no approval. About four minutes.
				</p>
 
				<div className="mt-6 flex flex-wrap items-center gap-2.5">
					<Button asChild>
						<Link to="/projects">Add a project</Link>
					</Button>
					<Button asChild variant="outline">
						{/*
						 * An example of the thing being asked for, on the surface it
						 * will appear on. There is no example project inside the
						 * portal to point at, and inventing one would be a fixture
						 * pretending to be the product.
						 */}
						<a
							href={`${MARKETPLACE_URL}/projects`}
							target="_blank"
							rel="noopener noreferrer"
						>
							See an example <span aria-hidden="true">&#9656;</span>
							<span className="sr-only">(opens the public marketplace)</span>
						</a>
					</Button>
				</div>
 
				<Separator className="my-6" />
 
				<PhaseRail phases={buildPublishPhases(readiness)} />
			</div>
		</Card>
	);
}