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()}
{consultation.role}
{consultation.examination.map((bullet, index) => (
-
{bullet}
))}
)
}
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 (
)
}