Files
Parsons/src/components/pages/ComparisonPage/ComparisonPageV1.tsx
Richie e89ac360e8 Extract ComparisonColumnCard + ComparisonTabCard molecules, refine comparison UI
- New molecule: ComparisonColumnCard — desktop column header card extracted
  from ComparisonTable (~150 lines removed from organism)
- New molecule: ComparisonTabCard — mobile tab rail card extracted from
  ComparisonPage (shared by V1 and V2)
- CellValue "unknown" restyled: icon+text in neutral grey (was Badge),
  InfoOutlinedIcon on right at 14px matching item info icons
- Unverified provider story data: all items set to unknown across all
  story files (no dashes in essentials)
- Mobile tab rail: recommended badge (replaces star), package price,
  shadow/glow, center-on-select scroll, overflow clipping fixed
- ComparisonPackageCard: added shadow, reduced CTA button to medium
- ComparisonTable first column: inline info icon pattern (non-breaking
  space + nowrap span) prevents icon orphaning on line wrap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:10:17 +10:00

231 lines
8.3 KiB
TypeScript

import React, { useId, useState, useRef, useCallback } from 'react';
import Box from '@mui/material/Box';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { WizardLayout } from '../../templates/WizardLayout';
import { ComparisonTable, type ComparisonPackage } from '../../organisms/ComparisonTable';
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
import { ComparisonTabCard } from '../../molecules/ComparisonTabCard';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the ComparisonPageV1 */
export interface ComparisonPageV1Props {
/** User-selected packages to compare (max 3) */
packages: ComparisonPackage[];
/** System-recommended package — always shown as an additional column */
recommendedPackage?: ComparisonPackage;
/** Called when user clicks CTA on a package */
onArrange: (packageId: string) => void;
/** Called when user removes a package from comparison */
onRemove: (packageId: string) => void;
/** Called when user clicks Back */
onBack: () => void;
/** Called when user clicks Share */
onShare?: () => void;
/** Called when user clicks Print */
onPrint?: () => void;
/** Navigation bar slot */
navigation?: React.ReactNode;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ──────────────────────────────────────────────────────────────
/**
* **Archived — V1.** See `ComparisonPage.tsx` (V2) for the production version.
*
* Package comparison page for the FA design system.
*
* Desktop: Full ComparisonTable with info card, floating verified badges,
* section tables with left accent borders. Recommended package appears as the
* **last** column.
* Mobile: Tabbed card view with horizontal chip rail. Recommended package is
* the last tab.
*
* Share + Print utility actions in the page header.
*/
export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV1Props>(
(
{ packages, recommendedPackage, onArrange, onRemove, onBack, onShare, onPrint, navigation, sx },
ref,
) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const tablistId = useId();
const railRef = useRef<HTMLDivElement>(null);
const tabRefs = useRef<(HTMLDivElement | null)[]>([]);
const allPackages = React.useMemo(() => {
const result = [...packages];
if (recommendedPackage) {
result.push({ ...recommendedPackage, isRecommended: true });
}
return result;
}, [packages, recommendedPackage]);
const [activeTabIdx, setActiveTabIdx] = useState(0);
const activePackage = allPackages[activeTabIdx] ?? allPackages[0];
const providerCount = new Set(allPackages.map((p) => p.provider.name)).size;
const subtitle =
providerCount > 1
? `Comparing ${packages.length} package${packages.length !== 1 ? 's' : ''} from different providers`
: `Comparing ${packages.length} package${packages.length !== 1 ? 's' : ''}`;
const hasRecommended = allPackages.some((p) => p.isRecommended);
const scrollToCenter = useCallback((idx: number) => {
const tab = tabRefs.current[idx];
if (tab && railRef.current) {
const rail = railRef.current;
const tabCenter = tab.offsetLeft + tab.offsetWidth / 2;
const railCenter = rail.offsetWidth / 2;
rail.scrollTo({ left: tabCenter - railCenter, behavior: 'smooth' });
}
}, []);
const handleTabClick = useCallback(
(idx: number) => {
setActiveTabIdx(idx);
scrollToCenter(idx);
},
[scrollToCenter],
);
// Center the default tab on mount
React.useEffect(() => {
const timer = setTimeout(() => scrollToCenter(0), 50);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Box ref={ref} sx={sx}>
<WizardLayout
variant="wide-form"
navigation={navigation}
showBackLink
backLabel="Back"
onBack={onBack}
>
{/* Page header with Share/Print actions */}
<Box sx={{ mb: { xs: 3, md: 5 } }}>
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: 2,
flexWrap: 'wrap',
}}
>
<Box>
<Typography variant="h2" component="h1" sx={{ mb: 1 }}>
Compare packages
</Typography>
<Typography variant="body1" color="text.secondary" aria-live="polite">
{subtitle}
</Typography>
</Box>
{/* Share + Print */}
{(onShare || onPrint) && (
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
{onShare && (
<Button
variant="outlined"
color="secondary"
size="small"
startIcon={<ShareOutlinedIcon />}
onClick={onShare}
>
Share
</Button>
)}
{onPrint && (
<Button
variant="outlined"
color="secondary"
size="small"
startIcon={<PrintOutlinedIcon />}
onClick={onPrint}
>
Print
</Button>
)}
</Box>
)}
</Box>
</Box>
{/* Desktop: ComparisonTable */}
{!isMobile && (
<ComparisonTable packages={allPackages} onArrange={onArrange} onRemove={onRemove} />
)}
{/* Mobile: Tab rail + card view */}
{isMobile && allPackages.length > 0 && (
<>
{/* Tab rail — mini cards showing provider + package + price */}
<Box
ref={railRef}
role="tablist"
id={tablistId}
aria-label="Packages to compare"
sx={{
display: 'flex',
gap: 1.5,
overflowX: 'auto',
py: 2,
px: 2,
mx: -2,
mt: 1,
mb: 3,
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
WebkitOverflowScrolling: 'touch',
}}
>
{allPackages.map((pkg, idx) => (
<ComparisonTabCard
key={pkg.id}
ref={(el: HTMLDivElement | null) => {
tabRefs.current[idx] = el;
}}
pkg={pkg}
isActive={idx === activeTabIdx}
hasRecommended={hasRecommended}
tabId={`comparison-tab-${idx}`}
tabPanelId={`comparison-tabpanel-${idx}`}
onClick={() => handleTabClick(idx)}
/>
))}
</Box>
{activePackage && (
<Box
role="tabpanel"
id={`comparison-tabpanel-${activeTabIdx}`}
aria-labelledby={`comparison-tab-${activeTabIdx}`}
>
<ComparisonPackageCard pkg={activePackage} onArrange={onArrange} />
</Box>
)}
</>
)}
</WizardLayout>
</Box>
);
},
);
ComparisonPageV1.displayName = 'ComparisonPageV1';
export default ComparisonPageV1;