diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index 95391b0..fc22c34 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -6,6 +6,7 @@ import { PatientSummaryTile } from './tiles/PatientSummaryTile' import { LatestResultsTile } from './tiles/LatestResultsTile' import { CoreSkillsTile } from './tiles/CoreSkillsTile' import { LastConsultationTile } from './tiles/LastConsultationTile' +import { CareerActivityTile } from './tiles/CareerActivityTile' const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches @@ -109,6 +110,8 @@ export function DashboardLayout() { {/* CareerActivityTile — full width */} + + {/* EducationTile — full width */} {/* ProjectsTile — full width */} diff --git a/src/components/tiles/CareerActivityTile.tsx b/src/components/tiles/CareerActivityTile.tsx new file mode 100644 index 0000000..7baec2e --- /dev/null +++ b/src/components/tiles/CareerActivityTile.tsx @@ -0,0 +1,228 @@ +import React from 'react' +import { Card, CardHeader } from '../Card' +import { documents } from '@/data/documents' + +type ActivityType = 'role' | 'project' | 'cert' | 'edu' + +interface ActivityEntry { + id: string + type: ActivityType + title: string + meta: string + date: string + sortYear: number +} + +/** + * Build timeline from multiple data sources + * Matches the concept HTML entries exactly + */ +function buildTimeline(): ActivityEntry[] { + const entries: ActivityEntry[] = [] + + // Roles from consultations + // Entry 1: Interim Head (2024-2025) + 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, + }) + + // Entry 3: Senior Data Analyst (2021-2024) - concept calls this "Senior Data Analyst — Medicines Optimisation" + entries.push({ + id: 'deputy-head-2024', + type: 'role', + title: 'Senior Data Analyst — Medicines Optimisation', + meta: 'NHS Norfolk & Waveney ICB', + date: '2021 – 2024', + sortYear: 2021, + }) + + // Entry 6: Prescribing Data Pharmacist (2018-2021) + entries.push({ + id: 'high-cost-drugs-2022', + type: 'role', + title: 'Prescribing Data Pharmacist', + meta: 'NHS Norwich CCG', + date: '2018 – 2021', + sortYear: 2018, + }) + + // Entry 8: Community Pharmacist (2016-2018) - from Tesco roles + entries.push({ + id: 'pharmacy-manager-2017', + type: 'role', + title: 'Community Pharmacist', + meta: 'Boots UK', + date: '2016 – 2018', + sortYear: 2016, + }) + + // Projects from investigations + // Entry 2: £220M Prescribing Budget Oversight (2024) + entries.push({ + id: 'inv-budget', + type: 'project', + title: '£220M Prescribing Budget Oversight', + meta: 'Lead analyst & budget owner', + date: '2024', + sortYear: 2024, + }) + + // Entry 4: SQL Analytics Transformation (2025) + entries.push({ + id: 'inv-sql-transform', + type: 'project', + title: 'SQL Analytics Transformation', + meta: 'Legacy migration project lead', + date: '2025', + sortYear: 2025, + }) + + // Certifications from documents + // Entry 5: Power BI Data Analyst Associate (2023) + entries.push({ + id: 'cert-powerbi', + type: 'cert', + title: 'Power BI Data Analyst Associate', + meta: 'Microsoft Certified', + date: '2023', + sortYear: 2023, + }) + + // Entry 7: Clinical Pharmacy Diploma (2019) + entries.push({ + id: 'cert-diploma', + type: 'cert', + title: 'Clinical Pharmacy Diploma', + meta: 'Professional development', + date: '2019', + sortYear: 2019, + }) + + // Entry 10: GPhC Registration (2016) + entries.push({ + id: 'doc-gphc', + type: 'cert', + title: 'GPhC Registration', + meta: 'General Pharmaceutical Council', + date: 'August 2016', + sortYear: 2016, + }) + + // Education from documents + // Entry 9: MPharm (2011-2015) + 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, + }) + } + + // Sort newest-first by sortYear (descending), then by entry order for same year + return entries.sort((a, b) => { + if (b.sortYear !== a.sortYear) return b.sortYear - a.sortYear + // For same year, preserve insertion order (stable sort) + return 0 + }) +} + +const dotColorMap: Record = { + role: '#0D6E6E', // teal (--accent) + project: '#D97706', // amber + cert: '#059669', // green (--success) + edu: '#7C3AED', // purple +} + +interface ActivityItemProps { + entry: ActivityEntry +} + +const ActivityItem: React.FC = ({ entry }) => { + const dotColor = dotColorMap[entry.type] + + return ( +
+ {/* Dot */} +
+ + {/* Content */} +
+
+ {entry.title} +
+
+ {entry.meta} +
+
+ {entry.date} +
+
+
+ ) +} + +export const CareerActivityTile: React.FC = () => { + const timeline = buildTimeline() + + return ( + + + + {/* Activity grid - 2 columns on desktop, 1 on mobile */} +
+ {timeline.map((entry) => ( + + ))} +
+
+ ) +} diff --git a/src/index.css b/src/index.css index 1a8fba5..cb4968c 100644 --- a/src/index.css +++ b/src/index.css @@ -276,3 +276,16 @@ html { grid-template-columns: 1fr; } } + +/* Activity grid responsive */ +.activity-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +@media (max-width: 900px) { + .activity-grid { + grid-template-columns: 1fr; + } +}