feat(frontend): Spectrum UX port — Tabs + AlertBanner + BackLink + compact tables (sprint 11) #13
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user