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 | 61x 61x 61x 1x 60x | import { Link } from "@tanstack/react-router";
import { ArrowLeft, ExternalLink } from "lucide-react";
import { MARKETPLACE_URL } from "../../lib/env";
import { formatRelativeDate } from "../../lib/format-date";
import { Button } from "../ui/button";
import { StatusBadge } from "../ui/status-badge";
import { ProjectActionsMenu } from "./ProjectActionsMenu";
import { projectStatusCode } from "./project-status";
import type { ProjectHeaderProps } from "./types";
/**
* The project detail header — `project-detail-details.html` /
* `project-detail-photos.html`.
*
* ← Projects
* KONDAPUR 3BHK [LIVE] [ ⋯ ] [ View ↗ ]
* 23 photos · updated 2 days ago
*
* The page title is `.title` (~1.375rem cap, `text-xl` is the token under it);
* the display ramp is for empty states and auth. The status is a printed code,
* never a pill.
*
* EVERY ACTION, AT EVERY STATUS
* The old header rendered Publish only on a `draft` and Archive only on a
* `published` project, which left an archived project's detail page with no
* actions at all. The whole set now lives in one `[ ⋯ ]` menu that is present
* at every status — see `project-actions.ts` for why a disabled item with a
* printed reason beats a hidden one.
*/
export function ProjectHeader({
project,
pro,
context,
backLink,
isSaving,
photoCount = 0,
publishBlockedBy,
canMoveToDraft = false,
canDelete = false,
onAction,
mobileActionsOnly,
}: ProjectHeaderProps) {
const code = projectStatusCode(project.status);
const actions = (
<>
<ProjectActionsMenu
status={project.status}
publishBlockedBy={publishBlockedBy}
canMoveToDraft={canMoveToDraft}
canDelete={canDelete}
isBusy={isSaving}
onAction={onAction}
/>
{project.status === "published" && (
<Button asChild variant="outline" size="sm" className="h-8">
<a
href={`${MARKETPLACE_URL}/projects/${project.id}`}
target="_blank"
rel="noreferrer"
>
View
<ExternalLink aria-hidden="true" />
<span className="sr-only">opens in a new tab</span>
</a>
</Button>
)}
</>
);
// Mobile full-screen mode: the title already sits in the sticky bar, so
// only the action cluster is rendered here.
if (mobileActionsOnly) {
return (
<div className="flex items-center gap-2">
<StatusBadge status={code.label} variant={code.tone} />
{actions}
</div>
);
}
return (
<div>
<Link
to={backLink}
className="inline-flex items-center gap-1 text-sm font-semibold text-foreground-muted underline-offset-4 hover:text-foreground-default hover:underline"
>
<ArrowLeft aria-hidden="true" className="size-4" />
Projects
</Link>
<div className="mt-2 flex flex-wrap items-start justify-between gap-3">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h1 className="font-heading text-xl font-bold break-words text-foreground-default">
{project.title}
</h1>
<StatusBadge status={code.label} variant={code.tone} />
</div>
<div className="flex shrink-0 items-center gap-2">{actions}</div>
</div>
<p className="mt-1 text-sm text-foreground-muted">
<span className="tabular-nums">{photoCount}</span>{" "}
{photoCount === 1 ? "photo" : "photos"} · updated{" "}
{formatRelativeDate(project.dateUpdated)}
</p>
{context === "admin" && pro && (
<p className="mt-1 text-sm text-foreground-muted">
by{" "}
<Link
to={`/admin/pros/${pro.id}`}
className="font-semibold text-foreground-default underline-offset-4 hover:underline"
>
{pro.businessName}
</Link>
</p>
)}
</div>
);
}
|