All files / src/components/projects ProjectHeader.tsx

100% Statements 5/5
100% Branches 12/12
100% Functions 1/1
100% Lines 5/5

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>
	);
}