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 | 3x 169x 169x 169x 169x 169x 8x 6x | import { memo } from "react";
import { Link } from "@tanstack/react-router";
import { Star, StarOff, Eye } from "lucide-react";
import { Button } from "../../ui/button";
import { Select } from "../../ui/select";
import {
STATUS_CODE_TYPE,
STATUS_INK,
STATUS_TINT,
type StatusTone,
} from "../../ui/status-ink";
import { TableCell, TableRow } from "../../ui/table";
import { cn } from "../../../lib/utils";
import type { Pro } from "../../../lib/api";
interface ProTableRowProps {
pro: Pro;
onToggleFeatured: (pro: Pro) => void;
onUpdateStatus: (
pro: Pro,
status: "draft" | "published" | "archived",
) => void;
}
export const ProTableRow = memo(function ProTableRow({
pro,
onToggleFeatured,
onUpdateStatus,
}: ProTableRowProps) {
// #360: admins reported "blank" rows at the top of the Pros table.
// These were pros whose onboarding flow never filled businessName (draft
// state), so the DB stores an empty string. Rather than render nothing —
// which makes the row unclickable-looking — fall back to the pro's slug
// or id in muted type so admins can still identify and open the record.
const trimmedName = pro.businessName?.trim();
const displayName = trimmedName || pro.slug || pro.id;
const isMissingName = !trimmedName;
// The printed code's ink, from the one module that decides status colour.
// `archived` reads as the neutral code rather than an error: it is a
// deliberate admin state, not a failure.
const statusTone: StatusTone =
pro.status === "published"
? "success"
: pro.status === "draft"
? "warning"
: "default";
return (
<TableRow>
<TableCell>
{/*
* The status cell is a printed code that is ALSO the control that
* changes it — changing publish status is the admin's whole job on
* this screen, and splitting "what it is" from "set it to" would
* cost a column and a scan. It keeps the printed-code vocabulary
* (2px radius, `.label` caps, status ink on its own tint, never a
* pill's full radius) rather than inventing a third shape.
*
* The archive confirmation lives in `pros.tsx`'s `updateStatus`, so
* it survives this control's shape.
*/}
<Select
tier="dense"
aria-label={`Publish status for ${displayName}`}
value={pro.status}
onChange={(e) =>
onUpdateStatus(
pro,
e.target.value as "draft" | "published" | "archived",
)
}
className={cn(
"w-auto min-w-28 cursor-pointer rounded-sm border-transparent",
STATUS_CODE_TYPE,
STATUS_TINT[statusTone],
STATUS_INK[statusTone],
)}
>
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="archived">Archived</option>
</Select>
</TableCell>
<TableCell>
<Link
to={`/admin/pros/${pro.id}`}
className={`font-medium hover:text-primary-600 ${
isMissingName
? "text-foreground-muted italic"
: "text-foreground-default"
}`}
>
{displayName}
{isMissingName && (
<span className="ml-1 text-xs text-foreground-subtle not-italic">
(no business name)
</span>
)}
</Link>
</TableCell>
<TableCell className="text-foreground-muted">
{pro.primaryLocality
? `${pro.primaryLocality.name}, ${pro.primaryLocality.city}`
: "-"}
</TableCell>
<TableCell>
<button
type="button"
onClick={() => onToggleFeatured(pro)}
className="rounded-sm p-1 hover:bg-background-muted"
title={pro.isFeatured ? "Remove from featured" : "Add to featured"}
>
{pro.isFeatured ? (
<Star className="h-5 w-5 fill-rating text-rating" />
) : (
<StarOff className="h-5 w-5 text-foreground-subtle" />
)}
</button>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Link to={`/admin/pros/${pro.id}`}>
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
<span className="sr-only">View profile</span>
</Button>
</Link>
</div>
</TableCell>
</TableRow>
);
});
|