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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 2x 44x 35x 33x 41x 41x 41x 41x 41x 41x 38x | import type { Pro, Project } from "../../lib/api";
import type { PublishReadiness } from "../../hooks/queries/usePublishReadiness";
import { MARKETPLACE_URL } from "../../lib/env";
import { cn } from "../../lib/utils";
import { Card } from "../ui/card";
import { StatusBadge } from "../ui/status-badge";
import { WORK_TONE } from "./work-queue";
/**
* YOUR STUDIO — the panel that replaced the "pending review" banner.
*
* Plan §1.3; `.context/mockups/today.html` (the `<aside class="card">` beside
* the figure strip).
*
* WHAT IT SAYS, AND WHY IT IS NOT A BANNER
* The deleted banner said "Your profile is pending review … Contact admin for
* more details." There is no review and there is nothing to contact admin
* about: a studio that clears the bar is published by the server
* automatically. So the two honest things to print are (a) you are live, here
* is the address, or (b) you are not live yet, and here is exactly what is
* missing — **in the server's own words**.
*
* THE SERVER OWNS THE COPY FOR (b)
* `readiness.failing[].label` is rendered verbatim. The portal does not write
* "Add three photos" anywhere, because then there would be two places that
* describe the bar and one of them would eventually be wrong. The `blocks`
* flag is printed as a distinction the pro can act on: one requirement
* refuses a publish, the other four are advisory and a publish still
* succeeds without them.
*
* THE COUNTS
* "8 projects · 2 featured" — projects and FEATURED PROJECTS. The mockup says
* "2 rooms featured"; a room-level featured count needs one room fetch per
* project, which is the N+1 the "no new data" constraint rules out. The
* project-level flag is on the list query the page already makes, and it is
* labelled for what it is rather than mislabelled as rooms.
*/
const LABEL = "text-xs font-semibold uppercase tracking-wider";
/** `draft`/`published`/`archived` → the printed code. */
export function studioCode(
status: Pro["status"],
): "Live" | "Draft" | "Archived" {
if (status === "published") return "Live";
if (status === "archived") return "Archived";
return "Draft";
}
export interface StudioCardProps {
pro: Pro;
projects: readonly Project[];
readiness: PublishReadiness | null | undefined;
}
export function StudioCard({ pro, projects, readiness }: StudioCardProps) {
const code = studioCode(pro.status);
const isLive = code === "Live";
const publicUrl = pro.slug ? `${MARKETPLACE_URL}/pros/${pro.slug}` : null;
const featured = projects.filter((project) => project.isFeatured).length;
const failing = readiness?.failing ?? [];
return (
<Card className="overflow-hidden">
<div className="flex items-center gap-2.5 border-b border-border-default px-4 py-2.5">
<span className={cn(LABEL, "grow text-foreground-muted")}>
Your studio
</span>
{/*
* The tick is part of the printed code's text ("✓ LIVE" in the
* mockup), not a slot: `StatusBadge` takes the word and owns the
* ink, which is what keeps every code in the portal identical.
*/}
<StatusBadge
status={isLive ? "✓ Live" : code}
variant={WORK_TONE[code]}
/>
</div>
<div className="space-y-2.5 px-4 py-3.5">
{isLive && publicUrl ? (
<>
<p className="truncate text-sm text-foreground-default">
<code>{publicUrl.replace(/^https?:\/\//, "")}</code>
</p>
<p className={cn(LABEL, "text-foreground-muted")}>
<span className="tabular-nums">{projects.length}</span> projects
· <span className="tabular-nums">{featured}</span> featured
</p>
<p>
<a
href={publicUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-semibold text-primary-600 underline decoration-2 underline-offset-4 hover:text-primary-700"
>
View your page <span aria-hidden="true">↗</span>
<span className="sr-only">(opens the public marketplace)</span>
</a>
</p>
</>
) : (
<>
<p className="text-sm text-foreground-muted">
{code === "Archived"
? "Your studio has been taken off the marketplace. Talk to us and we will put it back."
: "Your studio is not on the marketplace yet. It goes live by itself the moment these are done — nobody reviews it."}
</p>
{failing.length > 0 && (
<ul className="space-y-0">
{failing.map((requirement) => (
<li
key={requirement.id}
className="flex items-start gap-2 border-t border-border-default py-1.5 text-sm text-foreground-default first:border-t-0"
>
<span
aria-hidden="true"
className="w-4 shrink-0 text-center text-foreground-subtle"
>
·
</span>
<span className="grow">{requirement.label}</span>
{requirement.blocks && (
<StatusBadge
status="Required"
variant="error"
className="shrink-0"
/>
)}
</li>
))}
</ul>
)}
</>
)}
</div>
<div className="border-t border-border-default bg-background-subtle px-4 py-2">
<p className={cn(LABEL, "truncate text-foreground-muted")}>
{pro.businessName}
{pro.primaryLocality?.city ? ` · ${pro.primaryLocality.city}` : ""}
</p>
</div>
</Card>
);
}
|