From 1b1908778230e165e446a2b65bf2bf39684fc410 Mon Sep 17 00:00:00 2001 From: Andy Charlwood Date: Tue, 17 Feb 2026 02:16:10 +0000 Subject: [PATCH] refactor: extract LastConsultationCard from DashboardLayout Move the self-contained LastConsultationSubsection component (191 lines) into its own file as LastConsultationCard. It uses only context and one prop, with no dependency on DashboardLayout state. DashboardLayout drops from 493 to 293 lines. --- src/components/DashboardLayout.tsx | 207 +----------------------- src/components/LastConsultationCard.tsx | 204 +++++++++++++++++++++++ 2 files changed, 208 insertions(+), 203 deletions(-) create mode 100644 src/components/LastConsultationCard.tsx diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index d8dd334..c9d30e4 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -1,24 +1,22 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' +import { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { motion } from 'framer-motion' -import { ChevronRight } from 'lucide-react' import Sidebar from './Sidebar' import { CommandPalette } from './CommandPalette' import { DetailPanel } from './DetailPanel' -import { CardHeader } from './Card' import { PatientSummaryTile } from './tiles/PatientSummaryTile' import { ProjectsTile } from './tiles/ProjectsTile' import { ParentSection } from './ParentSection' import CareerConstellation from './constellation/CareerConstellation' import { TimelineInterventionsSubsection } from './TimelineInterventionsSubsection' import { RepeatMedicationsSubsection } from './RepeatMedicationsSubsection' +import { LastConsultationCard } from './LastConsultationCard' import { ChatWidget } from './ChatWidget' import { useActiveSection } from '@/hooks/useActiveSection' import { useDetailPanel } from '@/contexts/DetailPanelContext' import { timelineConsultations } from '@/data/timeline' import { skills } from '@/data/skills' import type { PaletteAction } from '@/lib/search' -import { hexToRgba, prefersReducedMotion, motionSafeTransition } from '@/lib/utils' -import { DEFAULT_ORG_COLOR } from '@/lib/theme-colors' +import { prefersReducedMotion, motionSafeTransition } from '@/lib/utils' const sidebarVariants = { hidden: prefersReducedMotion ? { x: 0, opacity: 1 } : { x: -272, opacity: 0 }, @@ -37,203 +35,6 @@ const contentVariants = { }, } -interface LastConsultationSubsectionProps { - highlightedRoleId?: string | null -} - -function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubsectionProps) { - const { openPanel } = useDetailPanel() - const consultation = timelineConsultations[0] - if (!consultation) { - return null - } - const isHighlighted = highlightedRoleId === consultation.id - - const handleOpenPanel = () => { - openPanel({ type: 'consultation', consultation }) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - handleOpenPanel() - } - } - - const formatDate = (dateStr: string): string => { - const date = new Date(dateStr) - return date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }) - } - - const getEmploymentType = (): string => { - if (consultation.organization.includes('ICB')) { - return 'Permanent · Full-time' - } - return 'Permanent' - } - - const getBand = (): string => { - if (consultation.role.includes('Head')) { - return '8a' - } - return '—' - } - - const fieldLabelStyle: React.CSSProperties = { - fontSize: '12px', - textTransform: 'uppercase', - letterSpacing: '0.06em', - color: 'var(--text-tertiary)', - marginBottom: '3px', - } - - const fieldValueStyle: React.CSSProperties = { - fontSize: '13px', - fontWeight: 600, - color: 'var(--text-primary)', - } - - return ( -
- - -
{ - e.currentTarget.style.backgroundColor = hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.04) - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent' - }} - aria-label={`View full details for ${consultation.role}`} - > -
-
Date
-
{formatDate(consultation.date)}
-
-
-
Organisation
-
{consultation.organization}
-
-
-
Type
-
{getEmploymentType()}
-
-
-
Band
-
{getBand()}
-
-
- -
- {consultation.role} -
- -
    - {consultation.examination.map((bullet, index) => ( -
  • -
  • - ))} -
- - -
- ) -} - export function DashboardLayout() { const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) const [highlightedNodeId, setHighlightedNodeId] = useState(null) @@ -446,7 +247,7 @@ export function DashboardLayout() {
- +
diff --git a/src/components/LastConsultationCard.tsx b/src/components/LastConsultationCard.tsx new file mode 100644 index 0000000..255768f --- /dev/null +++ b/src/components/LastConsultationCard.tsx @@ -0,0 +1,204 @@ +import React from 'react' +import { ChevronRight } from 'lucide-react' +import { CardHeader } from './Card' +import { useDetailPanel } from '@/contexts/DetailPanelContext' +import { timelineConsultations } from '@/data/timeline' +import { hexToRgba } from '@/lib/utils' +import { DEFAULT_ORG_COLOR } from '@/lib/theme-colors' + +interface LastConsultationCardProps { + highlightedRoleId?: string | null +} + +export function LastConsultationCard({ highlightedRoleId }: LastConsultationCardProps) { + const { openPanel } = useDetailPanel() + const consultation = timelineConsultations[0] + if (!consultation) { + return null + } + const isHighlighted = highlightedRoleId === consultation.id + + const handleOpenPanel = () => { + openPanel({ type: 'consultation', consultation }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleOpenPanel() + } + } + + const formatDate = (dateStr: string): string => { + const date = new Date(dateStr) + return date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }) + } + + const getEmploymentType = (): string => { + if (consultation.organization.includes('ICB')) { + return 'Permanent · Full-time' + } + return 'Permanent' + } + + const getBand = (): string => { + if (consultation.role.includes('Head')) { + return '8a' + } + return '—' + } + + const fieldLabelStyle: React.CSSProperties = { + fontSize: '12px', + textTransform: 'uppercase', + letterSpacing: '0.06em', + color: 'var(--text-tertiary)', + marginBottom: '3px', + } + + const fieldValueStyle: React.CSSProperties = { + fontSize: '13px', + fontWeight: 600, + color: 'var(--text-primary)', + } + + return ( +
+ + +
{ + e.currentTarget.style.backgroundColor = hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.04) + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = 'transparent' + }} + aria-label={`View full details for ${consultation.role}`} + > +
+
Date
+
{formatDate(consultation.date)}
+
+
+
Organisation
+
{consultation.organization}
+
+
+
Type
+
{getEmploymentType()}
+
+
+
Band
+
{getBand()}
+
+
+ +
+ {consultation.role} +
+ +
    + {consultation.examination.map((bullet, index) => ( +
  • +
  • + ))} +
+ + +
+ ) +}