feat(frontend): Spectrum UX port — Tabs + AlertBanner + BackLink + compact tables (sprint 11) #13

Merged
knacky merged 11 commits from sprint/11-spectrum-ux into main 2026-06-22 08:25:38 +00:00
Showing only changes of commit 004d075cad - Show all commits

View File

@@ -1,12 +1,17 @@
import { Link, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { extractApiError } from '@/api/client'; import { extractApiError } from '@/api/client';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useEngagement } from '@/hooks/useEngagements'; import { useEngagement } from '@/hooks/useEngagements';
import { useHashTab } from '@/hooks/useHashTab';
import { useEngagementSimulations } from '@/hooks/useSimulations';
import { LoadingState } from '@/components/LoadingState'; import { LoadingState } from '@/components/LoadingState';
import { ErrorState } from '@/components/ErrorState'; import { ErrorState } from '@/components/ErrorState';
import { StatusBadge } from '@/components/StatusBadge'; import { StatusBadge } from '@/components/StatusBadge';
import { SimulationList } from '@/components/SimulationList'; import { SimulationList } from '@/components/SimulationList';
import { ExportEngagementButton } from '@/components/ExportEngagementButton'; import { ExportEngagementButton } from '@/components/ExportEngagementButton';
import { BackLink } from '@/components/BackLink';
import { Tabs } from '@/components/Tabs';
import { Link } from 'react-router-dom';
export function EngagementDetailPage(): JSX.Element { export function EngagementDetailPage(): JSX.Element {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@@ -14,6 +19,9 @@ export function EngagementDetailPage(): JSX.Element {
const { canEditEngagements } = useAuth(); const { canEditEngagements } = useAuth();
const detail = useEngagement(numericId); const detail = useEngagement(numericId);
const simsQuery = useEngagementSimulations(numericId);
const [activeTab, setActiveTab] = useHashTab('schedule');
if (detail.isLoading) return <LoadingState label="Loading engagement…" />; if (detail.isLoading) return <LoadingState label="Loading engagement…" />;
if (detail.isError) { if (detail.isError) {
@@ -27,14 +35,19 @@ export function EngagementDetailPage(): JSX.Element {
if (!detail.data) return <ErrorState message="Engagement not found" />; if (!detail.data) return <ErrorState message="Engagement not found" />;
const eng = detail.data; const eng = detail.data;
const simCount = simsQuery.data?.length;
const tabs = [
{ id: 'schedule', label: 'Schedule' },
{ id: 'description', label: 'Description' },
{ id: 'simulations', label: 'Simulations', count: simCount },
];
return ( return (
<div className="flex flex-col gap-xl"> <div className="flex flex-col gap-xl">
<header className="flex items-start justify-between gap-md"> <header className="flex items-start justify-between gap-md">
<div className="flex flex-col gap-sm"> <div className="flex flex-col gap-sm">
<Link to="/engagements" className="btn-text-link text-[14px]"> <BackLink to="/engagements">Back to engagements</BackLink>
Back to engagements
</Link>
<h1 className="text-[32px] font-medium leading-none">{eng.name}</h1> <h1 className="text-[32px] font-medium leading-none">{eng.name}</h1>
<div className="flex items-center gap-md"> <div className="flex items-center gap-md">
<StatusBadge status={eng.status} /> <StatusBadge status={eng.status} />
@@ -53,7 +66,9 @@ export function EngagementDetailPage(): JSX.Element {
) : null} ) : null}
</header> </header>
<section className="grid grid-cols-1 md:grid-cols-2 gap-md"> <Tabs items={tabs} activeId={activeTab} onChange={setActiveTab} />
{activeTab === 'schedule' && (
<div className="card-product"> <div className="card-product">
<h2 className="text-[20px] font-medium mb-md">Schedule</h2> <h2 className="text-[20px] font-medium mb-md">Schedule</h2>
<dl className="grid grid-cols-2 gap-md text-[14px]"> <dl className="grid grid-cols-2 gap-md text-[14px]">
@@ -67,18 +82,20 @@ export function EngagementDetailPage(): JSX.Element {
<dd className="text-ink font-mono">{eng.created_at}</dd> <dd className="text-ink font-mono">{eng.created_at}</dd>
</dl> </dl>
</div> </div>
)}
{activeTab === 'description' && (
<div className="card-product"> <div className="card-product">
<h2 className="text-[20px] font-medium mb-md">Description</h2> <h2 className="text-[20px] font-medium mb-md">Description</h2>
<p className="text-[16px] text-charcoal whitespace-pre-line"> <p className="text-[16px] text-charcoal whitespace-pre-line">
{eng.description?.trim() ? eng.description : 'No description provided.'} {eng.description?.trim() ? eng.description : 'No description provided.'}
</p> </p>
</div> </div>
</section> )}
<section> {activeTab === 'simulations' && (
<SimulationList engagementId={eng.id} /> <SimulationList engagementId={eng.id} />
</section> )}
</div> </div>
); );
} }