import React, { useState, useEffect, useCallback, useRef } 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 './CareerConstellation' import { TimelineInterventionsSubsection } from './TimelineInterventionsSubsection' import { RepeatMedicationsSubsection } from './RepeatMedicationsSubsection' import { ChatWidget } from './ChatWidget' import { useActiveSection } from '@/hooks/useActiveSection' import { useDetailPanel } from '@/contexts/DetailPanelContext' import { consultations } from '@/data/consultations' import { skills } from '@/data/skills' import type { PaletteAction } from '@/lib/search' const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches const sidebarVariants = { hidden: prefersReducedMotion ? { x: 0, opacity: 1 } : { x: -272, opacity: 0 }, visible: { x: 0, opacity: 1, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.25, ease: 'easeOut', delay: 0.05 }, }, } const contentVariants = { hidden: prefersReducedMotion ? { opacity: 1 } : { opacity: 0 }, visible: { opacity: 1, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3, delay: 0.15 }, }, } function hexToRgba(hex: string, opacity: number): string { const r = parseInt(hex.slice(1, 3), 16) const g = parseInt(hex.slice(3, 5), 16) const b = parseInt(hex.slice(5, 7), 16) return `rgba(${r},${g},${b},${opacity})` } interface LastConsultationSubsectionProps { highlightedRoleId?: string | null } function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubsectionProps) { const { openPanel } = useDetailPanel() const consultation = consultations[0] 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 ?? '#0D6E6E', 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}
) } export function DashboardLayout() { const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) const [highlightedNodeId, setHighlightedNodeId] = useState(null) const [highlightedRoleId, setHighlightedRoleId] = useState(null) const [chronologyHeight, setChronologyHeight] = useState(null) const chronologyRef = useRef(null) const activeSection = useActiveSection() const { openPanel } = useDetailPanel() // Measure the chronology stream height so the constellation graph can match it useEffect(() => { const el = chronologyRef.current if (!el) return const observer = new ResizeObserver((entries) => { for (const entry of entries) { setChronologyHeight(entry.contentRect.height) } }) observer.observe(el) return () => observer.disconnect() }, []) const handlePaletteClose = useCallback(() => { setCommandPaletteOpen(false) }, []) const handleSearchClick = useCallback(() => { setCommandPaletteOpen(true) }, []) const scrollToSection = useCallback((tileId: string) => { const tileEl = document.querySelector(`[data-tile-id="${tileId}"]`) if (tileEl) { tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) } }, []) // Constellation graph handlers const handleRoleClick = useCallback( (roleId: string) => { const consultation = consultations.find((c) => c.id === roleId) if (consultation) { openPanel({ type: 'career-role', consultation }) } }, [openPanel], ) const handleSkillClick = useCallback( (skillId: string) => { const skill = skills.find((s) => s.id === skillId) if (skill) { openPanel({ type: 'skill', skill }) } }, [openPanel], ) const handleNodeHighlight = useCallback((id: string | null) => { setHighlightedNodeId(id) }, []) const handleNodeHover = useCallback((id: string | null) => { setHighlightedRoleId(id) }, []) // Global Ctrl+K listener to open command palette useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault() setCommandPaletteOpen(prev => !prev) } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, []) // Handle palette actions (scroll to tile, expand item, open link, download) const handlePaletteAction = useCallback((action: PaletteAction) => { switch (action.type) { case 'scroll': { scrollToSection(action.tileId) break } case 'expand': { const tileEl = document.querySelector(`[data-tile-id="${action.tileId}"]`) if (tileEl) { tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) // Dispatch a custom event that the tile can listen for to expand the item const expandEvent = new CustomEvent('palette-expand', { detail: { tileId: action.tileId, itemId: action.itemId }, }) document.dispatchEvent(expandEvent) } break } case 'link': { window.open(action.url, '_blank', 'noopener,noreferrer') break } case 'download': { // For now, open the CV file or trigger a download // This can be wired to an actual PDF when available window.open('/References/CV_v4.md', '_blank') break } case 'panel': { openPanel(action.panelContent) break } } }, [openPanel, scrollToSection]) return (
{ e.currentTarget.style.top = '0' }} onBlur={(e) => { e.currentTarget.style.top = '-48px' }} > Skip to main content
{/* PatientSummaryTile — full width (includes Latest Results subsection) */} {/* ProjectsTile — full width */} {/* Patient Pathway — parent section with constellation graph + subsections */}
Clinical Record Stream
Chronological role and education entries. Select items to inspect full records.
{/* Command palette overlay */} {/* Detail panel */} {/* Floating chat widget */}
) }