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 | 119x 36x 44x 36x 36x 144x 3x 25x 99x | import { cn } from "../../lib/utils";
import type { PublishReadiness } from "../../hooks/queries/usePublishReadiness";
/**
* The phase rail — `Account ✓ · Studio details ✓ · First project · · You're live`.
*
* PROGRESS READS AS A PHASE RAIL, NOT A PERCENTAGE. The screen this replaces
* printed `0 %` over six chores, which tells a pro how incomplete they are
* and nothing about what to do. Four named phases tell them where they stand
* and which one is next.
*
* EVERY BOOLEAN HERE COMES FROM THE SERVER
* `buildPublishPhases` reads the publish-readiness response and nothing else:
* `profileComplete` for the profile half, the `portfolio` requirements for the
* portfolio half, `studioStatus` for the last phase. It computes no part of
* the bar — that is defined once in `apps/api/src/lib/publish-readiness.ts`
* and this rail is one of its three readers.
*
* The four phase NAMES are this screen's structure, not a restatement of the
* server's copy. Where the server's wording is what matters — "At least 3
* photos on the project" — it is rendered verbatim, by `StudioCard`, from
* `failing[].label`.
*
* "Account" has no requirement behind it because there cannot be one: a pro
* reading this screen is signed in. Printing it unticked would be a lie, and
* leaving it out would lose the only phase that is already behind them.
*/
export type PhaseState = "done" | "current" | "todo";
export interface Phase {
name: string;
state: PhaseState;
}
/**
* The four phases, from the server's answer.
*
* The current phase is the first one not yet done — so exactly one phase is
* ever current, and once the studio is live none is.
*/
export function buildPublishPhases(
readiness: PublishReadiness | null | undefined,
): Phase[] {
const portfolio =
readiness?.requirements.filter((r) => r.group === "portfolio") ?? [];
const done = [
// Signed in, therefore done. See the module header.
true,
readiness?.profileComplete === true,
portfolio.length > 0 && portfolio.every((r) => r.passes),
readiness?.studioStatus === "published",
];
const names = ["Account", "Studio details", "First project", "You're live"];
const firstUnfinished = done.indexOf(false);
return names.map((name, index) => ({
name,
state: done[index]
? "done"
: index === firstUnfinished
? "current"
: "todo",
}));
}
const DOT = "h-2.5 w-2.5 shrink-0 rounded-full border-2";
/** The ruled rail. `aria-current="step"` marks the phase the pro is on. */
export function PhaseRail({ phases }: { phases: readonly Phase[] }) {
return (
<ol className="flex flex-wrap items-center gap-2 text-sm">
{phases.map((phase, index) => (
<li
key={phase.name}
className="flex min-w-0 flex-1 items-center gap-2"
aria-current={phase.state === "current" ? "step" : undefined}
>
{/* The ─── between two steps. Not drawn before the first. */}
{index > 0 && (
<span
aria-hidden="true"
className="h-0 min-w-5 flex-1 border-t border-border-strong"
/>
)}
<span
className={cn(
"inline-flex items-center gap-1.5 whitespace-nowrap",
phase.state === "done" && "text-foreground-muted",
phase.state === "current" &&
"font-semibold text-foreground-default",
phase.state === "todo" && "text-foreground-subtle",
)}
>
<span
aria-hidden="true"
className={cn(
DOT,
phase.state === "done" && "border-success bg-success",
phase.state === "current" &&
"border-foreground-default bg-foreground-default",
phase.state === "todo" && "border-current bg-transparent",
)}
/>
{phase.name}
{phase.state === "done" && (
<>
<span aria-hidden="true">✓</span>
<span className="sr-only">done</span>
</>
)}
</span>
</li>
))}
</ol>
);
}
|