All files / src/components/projects ProjectListItem.tsx

100% Statements 3/3
100% Branches 6/6
100% Functions 1/1
100% Lines 3/3

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                                                                          2x       31x   31x                                                                        
import { Link } from "@tanstack/react-router";
import { memo } from "react";
import type { Project } from "../../lib/api";
import { formatRelativeDate } from "../../lib/format-date";
import { StatusBadge } from "../ui/status-badge";
import { TableCell, TableRow } from "../ui/table";
import { projectStatusCode } from "./project-status";
 
/**
 * One row of the projects list view — `projects-list.html`, list view.
 *
 * The mockup prints the other half of the ▦/☰ toggle as ONE ruled table, no
 * chips: `Status · Project · Locality · Photos · Updated`, every figure
 * tabular and right-aligned, rows tinting on hover. Both of those come from
 * the `Table` family (`TableRow`'s hover tint, `TableCell`'s `numeric`), which
 * is why this is composed rather than hand-rolled — Phase 6 adds a grep test
 * forbidding a raw `<table>` outside `components/ui/`.
 *
 * NO ACTION COLUMN, AND THAT IS THE POINT
 * This row used to carry publish / archive / unarchive / delete, which is why
 * unarchive was unreachable: the default status filter excluded `archived`, so
 * the only row that could offer it never rendered. The full action set now
 * lives in the detail page's `[ ⋯ ]` menu at every status, where an archived
 * project can always come back. The mockup's table has no action column for
 * the same reason.
 */
 
export interface ProjectListItemProps {
	project: Project;
	/**
	 * The resolved locality name. Omitted when the project records no locality,
	 * or when it sits outside the studio's own city and so is not in the
	 * loaded taxonomy — an em dash is honest; a guess is not.
	 */
	localityName?: string | null;
}
 
export const ProjectListItem = memo(function ProjectListItem({
	project,
	localityName,
}: ProjectListItemProps) {
	const code = projectStatusCode(project.status);
 
	return (
		<TableRow>
			<TableCell>
				<StatusBadge status={code.label} variant={code.tone} />
			</TableCell>
			<TableCell>
				<Link
					to="/projects/$projectId"
					params={{ projectId: project.id }}
					search={{ tab: "" }}
					className="font-semibold text-foreground-default underline-offset-4 hover:underline"
					title={project.title}
				>
					{project.title}
				</Link>
			</TableCell>
			<TableCell className="text-foreground-muted">
				{localityName || <span aria-hidden="true">—</span>}
				{!localityName && <span className="sr-only">No locality recorded</span>}
			</TableCell>
			<TableCell numeric>
				{typeof project.photoCount === "number" ? (
					project.photoCount
				) : (
					<>
						<span aria-hidden="true">—</span>
						<span className="sr-only">Photo count unavailable</span>
					</>
				)}
			</TableCell>
			<TableCell numeric className="text-foreground-muted">
				{formatRelativeDate(project.dateUpdated)}
			</TableCell>
		</TableRow>
	);
});