Files
portfolio/src/components/tiles/CareerActivityTile.tsx
T

322 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useCallback } from 'react'
import { Card, CardHeader } from '../Card'
import { documents } from '@/data/documents'
import { consultations } from '@/data/consultations'
import { skills } from '@/data/skills'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import CareerConstellation from '../CareerConstellation'
type ActivityType = 'role' | 'project' | 'cert' | 'edu'
interface ActivityEntry {
id: string
type: ActivityType
title: string
meta: string
date: string
sortYear: number
/** ID of the corresponding consultation in consultations.ts (role entries only) */
consultationId?: string
}
/**
* Build timeline from multiple data sources
* Matches the concept HTML entries exactly
*/
function buildTimeline(): ActivityEntry[] {
const entries: ActivityEntry[] = []
// Roles from consultations
entries.push({
id: 'interim-head-2025',
type: 'role',
title: 'Interim Head, Population Health & Data Analysis',
meta: 'NHS Norfolk & Waveney ICB',
date: '2024 2025',
sortYear: 2024,
consultationId: 'interim-head-2025',
})
entries.push({
id: 'deputy-head-2024',
type: 'role',
title: 'Senior Data Analyst — Medicines Optimisation',
meta: 'NHS Norfolk & Waveney ICB',
date: '2021 2024',
sortYear: 2021,
consultationId: 'deputy-head-2024',
})
entries.push({
id: 'high-cost-drugs-2022',
type: 'role',
title: 'Prescribing Data Pharmacist',
meta: 'NHS Norwich CCG',
date: '2018 2021',
sortYear: 2018,
consultationId: 'pharmacy-manager-2017',
})
entries.push({
id: 'community-pharmacist-2016',
type: 'role',
title: 'Community Pharmacist',
meta: 'Boots UK',
date: '2016 2018',
sortYear: 2016,
consultationId: 'duty-pharmacist-2016',
})
// Projects
entries.push({
id: 'inv-budget',
type: 'project',
title: '£220M Prescribing Budget Oversight',
meta: 'Lead analyst & budget owner',
date: '2024',
sortYear: 2024,
})
entries.push({
id: 'inv-sql-transform',
type: 'project',
title: 'SQL Analytics Transformation',
meta: 'Legacy migration project lead',
date: '2025',
sortYear: 2025,
})
// Certifications
entries.push({
id: 'cert-powerbi',
type: 'cert',
title: 'Power BI Data Analyst Associate',
meta: 'Microsoft Certified',
date: '2023',
sortYear: 2023,
})
entries.push({
id: 'cert-diploma',
type: 'cert',
title: 'Clinical Pharmacy Diploma',
meta: 'Professional development',
date: '2019',
sortYear: 2019,
})
entries.push({
id: 'doc-gphc',
type: 'cert',
title: 'GPhC Registration',
meta: 'General Pharmaceutical Council',
date: 'August 2016',
sortYear: 2016,
})
// Education
const mpharm = documents.find((d) => d.id === 'doc-mpharm')
if (mpharm) {
entries.push({
id: mpharm.id,
type: 'edu',
title: 'MPharm (Hons) — 2:1',
meta: 'University of East Anglia',
date: '2011 2015',
sortYear: 2011,
})
}
return entries.sort((a, b) => {
if (b.sortYear !== a.sortYear) return b.sortYear - a.sortYear
return 0
})
}
const dotColorMap: Record<ActivityType, string> = {
role: '#0D6E6E',
project: '#D97706',
cert: '#059669',
edu: '#7C3AED',
}
interface ActivityItemProps {
entry: ActivityEntry
onItemClick: () => void
}
const ActivityItem: React.FC<ActivityItemProps> = ({ entry, onItemClick }) => {
const [isHovered, setIsHovered] = useState(false)
const dotColor = dotColorMap[entry.type]
const isClickable = entry.type === 'role' && entry.consultationId
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (!isClickable) return
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onItemClick()
}
},
[isClickable, onItemClick],
)
// Get consultation data for preview text
const consultation = isClickable
? consultations.find((c) => c.id === entry.consultationId)
: null
// Get preview text (first 1-2 lines from examination)
const previewText =
consultation && consultation.examination.length > 0
? consultation.examination[0]
: null
return (
<div
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
onClick={isClickable ? onItemClick : undefined}
onKeyDown={isClickable ? handleKeyDown : undefined}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
display: 'flex',
flexDirection: 'column',
background: 'var(--bg-dashboard)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-light)',
fontSize: '12px',
transition: 'all 0.15s ease-out',
cursor: isClickable ? 'pointer' : 'default',
transform: isHovered && isClickable ? 'translateY(-1px)' : 'none',
boxShadow: isHovered && isClickable
? '0 2px 8px rgba(26,43,42,0.08)'
: '0 1px 2px rgba(26,43,42,0.05)',
borderColor: isHovered && isClickable ? 'var(--accent-border)' : 'var(--border-light)',
}}
>
{/* Item header row */}
<div style={{ display: 'flex', gap: '10px', padding: '10px 12px' }}>
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: dotColor,
flexShrink: 0,
marginTop: '2px',
}}
aria-hidden="true"
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontWeight: 600,
color: 'var(--text-primary)',
lineHeight: 1.3,
}}
>
{entry.title}
</div>
<div
style={{
fontSize: '11px',
color: 'var(--text-secondary)',
marginTop: '2px',
}}
>
{entry.meta}
</div>
<div
style={{
fontSize: '10px',
fontFamily: 'var(--font-mono)',
color: 'var(--text-tertiary)',
marginTop: '3px',
}}
>
{entry.date}
</div>
{/* Hover preview text for roles */}
{isHovered && previewText && (
<div
style={{
fontSize: '11px',
color: 'var(--text-secondary)',
marginTop: '6px',
lineHeight: 1.4,
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{previewText}
</div>
)}
</div>
</div>
</div>
)
}
export const CareerActivityTile: React.FC = () => {
const timeline = buildTimeline()
const { openPanel } = useDetailPanel()
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 handleItemClick = useCallback(
(entry: ActivityEntry) => {
if (entry.type === 'role' && entry.consultationId) {
handleRoleClick(entry.consultationId)
}
},
[handleRoleClick],
)
return (
<Card full tileId="career-activity">
<CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" />
<div style={{ marginBottom: '20px' }}>
<CareerConstellation
onRoleClick={handleRoleClick}
onSkillClick={handleSkillClick}
/>
</div>
<div className="activity-grid">
{timeline.map((entry) => (
<ActivityItem
key={entry.id}
entry={entry}
onItemClick={() => handleItemClick(entry)}
/>
))}
</div>
</Card>
)
}