Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line Lucide icon JSX elements across 267 .tsx files in: - shared/, layout/, dashboard/ - admin/ (all sections) - clients/, berths/, yachts/, companies/, interests/, documents/ - reminders/, reservations/, residential/, expenses/, email/ The regex targeted only the safe pattern \`<IconName className="..." />\` (no other props, self-closing, capitalized component name). Every match inspected is a decorative companion to visible text or sits inside a button whose accessible name comes from \`aria-label\` / sr-only text — the icon itself should not be announced. Screen readers no longer double-read the icon + the adjacent label text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing @axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues to pass. Test suite stays at 1315/1315 vitest. typescript clean. Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups backlog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
157 lines
4.3 KiB
TypeScript
157 lines
4.3 KiB
TypeScript
'use client';
|
||
|
||
import Link from 'next/link';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { Loader2 } from 'lucide-react';
|
||
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@/components/ui/table';
|
||
import { EmptyState } from '@/components/shared/empty-state';
|
||
import { apiFetch } from '@/lib/api/client';
|
||
|
||
interface OwnedYachtRow {
|
||
id: string;
|
||
name: string;
|
||
hullNumber: string | null;
|
||
lengthFt: string | null;
|
||
widthFt: string | null;
|
||
lengthM: string | null;
|
||
widthM: string | null;
|
||
status: string;
|
||
}
|
||
|
||
interface YachtListResponse {
|
||
data: OwnedYachtRow[];
|
||
}
|
||
|
||
interface CompanyOwnedYachtsTabProps {
|
||
companyId: string;
|
||
portSlug: string;
|
||
}
|
||
|
||
const STATUS_COLORS: Record<string, string> = {
|
||
active: 'bg-green-100 text-green-800 border-green-300',
|
||
retired: 'bg-gray-100 text-gray-800 border-gray-300',
|
||
sold_away: 'bg-amber-100 text-amber-800 border-amber-300',
|
||
};
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
active: 'Active',
|
||
retired: 'Retired',
|
||
sold_away: 'Sold Away',
|
||
};
|
||
|
||
function formatDimensions(y: OwnedYachtRow): string | null {
|
||
if (y.lengthFt || y.widthFt) {
|
||
const length = y.lengthFt ?? '-';
|
||
const width = y.widthFt ?? '-';
|
||
return `${length} × ${width} ft`;
|
||
}
|
||
if (y.lengthM || y.widthM) {
|
||
const length = y.lengthM ?? '-';
|
||
const width = y.widthM ?? '-';
|
||
return `${length} × ${width} m`;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function CompanyOwnedYachtsTab({ companyId, portSlug }: CompanyOwnedYachtsTabProps) {
|
||
const { data, isLoading } = useQuery<OwnedYachtRow[]>({
|
||
queryKey: ['companies', companyId, 'owned-yachts'],
|
||
queryFn: async () => {
|
||
const params = new URLSearchParams({
|
||
ownerType: 'company',
|
||
ownerId: companyId,
|
||
page: '1',
|
||
limit: '50',
|
||
includeArchived: 'false',
|
||
order: 'desc',
|
||
});
|
||
const res = await apiFetch<YachtListResponse>(`/api/v1/yachts?${params.toString()}`);
|
||
return res.data;
|
||
},
|
||
});
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-12">
|
||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" aria-hidden />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const yachts = data ?? [];
|
||
|
||
if (yachts.length === 0) {
|
||
return (
|
||
<EmptyState
|
||
title="No yachts owned"
|
||
description="Yachts owned by this company will appear here."
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="rounded-md border">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>Name</TableHead>
|
||
<TableHead>Dimensions</TableHead>
|
||
<TableHead>Hull Number</TableHead>
|
||
<TableHead>Status</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{yachts.map((y) => {
|
||
const dims = formatDimensions(y);
|
||
const statusLabel = STATUS_LABELS[y.status] ?? y.status;
|
||
const statusColor =
|
||
STATUS_COLORS[y.status] ?? 'bg-muted text-muted-foreground border-muted';
|
||
return (
|
||
<TableRow key={y.id}>
|
||
<TableCell>
|
||
<Link
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
href={`/${portSlug}/yachts/${y.id}` as any}
|
||
className="font-medium text-primary hover:underline"
|
||
>
|
||
{y.name}
|
||
</Link>
|
||
</TableCell>
|
||
<TableCell>
|
||
{dims ? (
|
||
<span className="text-sm">{dims}</span>
|
||
) : (
|
||
<span className="text-muted-foreground">-</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
{y.hullNumber ? (
|
||
<span className="text-sm">{y.hullNumber}</span>
|
||
) : (
|
||
<span className="text-muted-foreground">-</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
<span
|
||
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${statusColor}`}
|
||
>
|
||
{statusLabel}
|
||
</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
);
|
||
}
|