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 | 196x 36x 8x | import { useQuery } from "@tanstack/react-query";
import { request } from "../../lib/api/base";
import { queryKeys } from "../../lib/query-keys";
import { STALE_1MIN } from "../../lib/stale-times";
/**
* The publish bar, read from the one place that defines it.
*
* ONE HOOK, ONE QUERY KEY, ONE TYPE — AND WHY THAT MATTERS HERE
* The bar (a complete profile plus one published project with at least
* `photoMinimum` photos) is defined exactly once, in
* `apps/api/src/lib/publish-readiness.ts`. That definition drives the project
* publish gate, the `draft → published` auto-publish transition, and every
* surface in the portal that says whether a studio is live.
*
* The portal briefly had THREE hooks for it — one in `useProjectQueries.ts`,
* one in `useTodayQueries.ts`, one in `pages/onboarding/` — each caching the
* same endpoint under a different query key, each with its own copy of the
* types. That is not a tidiness problem: `ProjectDetailContainer` invalidates
* the key after a publish, and with three keys the invalidation reached one
* reader out of three. Today's phase rail and `StudioCard` went on printing
* "not live yet" to a pro who had just gone live, which is precisely the
* failure the single server-side definition exists to prevent.
*
* So: every reader imports from here, and `queryKeys.pro.publishReadiness` is
* the only key this response is ever cached under. A second hook for this
* endpoint is a bug, not a convenience.
*
* WHY THE TYPES ARE MIRRORED AND NOT IMPORTED
* `packages/api-types` mirrors this wire shape as `PublishReadinessResponse`,
* but the portal does not depend on that workspace (it takes
* `@interioring/design-system` and `@interioring/utils`). Adding the
* dependency is an `apps/portal/package.json` edit plus a `bun install` in a
* shared worktree. The types below are therefore the portal's single mirror —
* one copy, not three — and they follow the API module if the bar changes.
*
* READ `photoMinimum`; NEVER HARDCODE THE NUMBER. It is `3` today, and the
* whole point of the server owning the bar is that it does not have to be.
*
* STALENESS
* One minute, the tightest of the three windows this replaced. The answer
* changes the moment a photo is uploaded, a project is published or the
* profile is saved, and a stale "not live yet" is the one thing these screens
* must not print after the pro has just fixed it. Mutations that move the bar
* should still invalidate `queryKeys.pro.publishReadiness(proId)` rather than
* wait this out.
*/
/** The five requirements, in the order the server returns them. */
export type PublishRequirementId =
| "profile_basics"
| "profile_services"
| "profile_location"
| "project_published"
| "project_photo_minimum";
/** Which half of the bar a requirement belongs to. Server-owned. */
export type PublishRequirementGroup = "profile" | "portfolio";
/**
* One evaluated requirement, exactly as `apps/api/src/lib/publish-readiness.ts`
* puts it on the wire. `label` is server-owned — render it, never restate it.
*/
export interface PublishRequirement {
id: PublishRequirementId;
group: PublishRequirementGroup;
/** Server-owned copy, so every surface says the same thing. */
label: string;
passes: boolean;
/** True for the one requirement that refuses a publish when it fails. */
blocks: boolean;
}
/** `GET /api/pro/:proId/publish-readiness`, inside the standard envelope. */
export interface PublishReadiness {
/** Every requirement passes — the studio has cleared the bar. */
ready: boolean;
/** The profile half of the bar passes. The one definition of "complete". */
profileComplete: boolean;
/** A blocking requirement fails — a publish request will be refused. */
blocked: boolean;
/** `PROJECT_PHOTO_MINIMUM`. Read this; never hardcode 3. */
photoMinimum: number;
requirements: PublishRequirement[];
/** The subset of `requirements` that does not pass, in the same order. */
failing: PublishRequirement[];
/** Current `pros.status`, so a caller knows if the studio is already live. */
studioStatus: "draft" | "published" | "archived";
}
/**
* Fetch the bar for a studio.
*
* Note the scope: the endpoint takes **no** projectId. It answers "is this
* studio over the bar", scoping the photo minimum to the best-stocked
* already-published project. A specific draft's photo count is counted on the
* page; `photoMinimum` is what this supplies.
*
* `retry: false` because a pro who has not finished onboarding may have no
* studio row for the bar to evaluate (404), and no caller treats this query as
* the reason a page fails to render — each falls back to its own "we do not
* know yet" state.
*/
export function usePublishReadiness(proId: string | null | undefined) {
return useQuery({
queryKey: queryKeys.pro.publishReadiness(proId ?? ""),
queryFn: async (): Promise<PublishReadiness | null> => {
const response = await request<PublishReadiness>(
`/api/pro/${proId}/publish-readiness`,
);
return response.data ?? null;
},
enabled: !!proId,
retry: false,
staleTime: STALE_1MIN,
});
}
|