feat: Build Experience section with timeline (Task 8)
- Create Experience.tsx component with vertical timeline layout - Add 5 roles from NHS and Tesco with bullet points - ECG waveform SVG decoration beside heading - Timeline dots filled for current roles - Hover effects on cards (scale, shadow, left border) - Scroll-triggered animations using useScrollReveal hook - Responsive: hide timeline line/dots on mobile - Fix useScrollReveal ref type for React 18+ compatibility
This commit is contained in:
+2
-6
@@ -5,6 +5,7 @@ import { ECGAnimation } from './components/ECGAnimation'
|
||||
import { FloatingNav } from './components/FloatingNav'
|
||||
import { Hero } from './components/Hero'
|
||||
import { Skills } from './components/Skills'
|
||||
import { Experience } from './components/Experience'
|
||||
|
||||
function App() {
|
||||
const [phase, setPhase] = useState<Phase>('boot')
|
||||
@@ -27,12 +28,7 @@ function App() {
|
||||
|
||||
<Skills />
|
||||
|
||||
<section id="experience" className="py-20">
|
||||
<h2 className="font-primary text-2xl font-bold text-heading text-center mb-8">
|
||||
Experience
|
||||
</h2>
|
||||
<p className="text-muted text-center">Experience section will be built in Task 8</p>
|
||||
</section>
|
||||
<Experience />
|
||||
|
||||
<section id="education" className="py-20">
|
||||
<h2 className="font-primary text-2xl font-bold text-heading text-center mb-8">
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { useScrollReveal } from '@/hooks/useScrollReveal'
|
||||
import type { Experience as ExperienceType } from '@/types'
|
||||
|
||||
const experiences: ExperienceType[] = [
|
||||
{
|
||||
role: 'Interim Head of Population Health & Data Analysis',
|
||||
org: 'NHS Norfolk & Waveney ICB',
|
||||
date: 'May 2025 — Nov 2025',
|
||||
bullets: [
|
||||
'Led team through organisational transition, maintaining delivery of £14.6M efficiency programme',
|
||||
'Directed strategic priorities for population health analytics across Norfolk & Waveney (population ~1M)',
|
||||
'Managed stakeholder relationships with system leaders, provider trusts, and primary care networks',
|
||||
],
|
||||
isCurrent: true,
|
||||
},
|
||||
{
|
||||
role: 'Deputy Head of Population Health & Data Analysis',
|
||||
org: 'NHS Norfolk & Waveney ICB',
|
||||
date: 'Jul 2024 — Present',
|
||||
bullets: [
|
||||
'Deputised for Head of department across all operational and strategic functions',
|
||||
'Oversaw £220M medicines budget and led programme of cost improvement initiatives',
|
||||
'Developed Python-based switching algorithm processing 14,000 patients, delivering £2.6M savings',
|
||||
'Built Blueteq automation system reducing processing time by 70%, saving 200+ hours annually',
|
||||
'Created PharMetrics dashboard platform for real-time medicines expenditure tracking',
|
||||
],
|
||||
isCurrent: true,
|
||||
},
|
||||
{
|
||||
role: 'High-Cost Drugs & Interface Pharmacist',
|
||||
org: 'NHS Norfolk & Waveney ICB',
|
||||
date: 'May 2022 — Jul 2024',
|
||||
bullets: [
|
||||
'Managed high-cost drugs budget across acute and community settings',
|
||||
'Led NICE Technology Appraisal implementation and horizon scanning',
|
||||
'Developed health economic models for biosimilar switching programmes',
|
||||
'Built data pipelines for automated reporting of medicines expenditure',
|
||||
],
|
||||
isCurrent: false,
|
||||
},
|
||||
{
|
||||
role: 'Pharmacy Manager',
|
||||
org: 'Tesco Pharmacy',
|
||||
date: 'Nov 2017 — May 2022',
|
||||
bullets: [
|
||||
'Managed community pharmacy delivering 3,000+ items monthly',
|
||||
'Pioneered asthma screening service generating £1M+ national revenue',
|
||||
'Led team of 6 through COVID-19 pandemic service delivery',
|
||||
'Completed Mary Seacole NHS Leadership Programme (2018)',
|
||||
],
|
||||
isCurrent: false,
|
||||
},
|
||||
{
|
||||
role: 'Duty Pharmacy Manager',
|
||||
org: 'Tesco Pharmacy',
|
||||
date: 'Aug 2016 — Nov 2017',
|
||||
bullets: [
|
||||
'Supported pharmacy manager in daily operations and clinical services',
|
||||
'Delivered Medicines Use Reviews and New Medicine Service consultations',
|
||||
'Maintained controlled drug compliance and clinical governance standards',
|
||||
],
|
||||
isCurrent: false,
|
||||
},
|
||||
]
|
||||
|
||||
const ECGDecoration = () => (
|
||||
<svg
|
||||
className="shrink-0 w-[200px] h-[30px] md:w-[200px] w-[120px]"
|
||||
viewBox="0 0 200 30"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 0 15 L 40 15 L 50 15 C 53 15 55 12 58 12 C 61 12 63 15 66 15 L 76 15 L 80 20 L 86 2 L 92 22 L 96 15 L 106 15 C 109 15 111 11 114 11 C 117 11 120 15 123 15 L 200 15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-teal opacity-30"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const TimelineEntry = ({
|
||||
experience,
|
||||
index,
|
||||
isVisible,
|
||||
}: {
|
||||
experience: ExperienceType
|
||||
index: number
|
||||
isVisible: boolean
|
||||
}) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="relative pl-0 md:pl-[calc(20%+32px)] mb-8 last:mb-0"
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<div
|
||||
className={`absolute left-[20%] top-2 -translate-x-1/2 w-2.5 h-2.5 rounded-full border-2 border-teal bg-white z-10 hidden md:block ${
|
||||
experience.isCurrent ? 'bg-teal' : ''
|
||||
}`}
|
||||
/>
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border-l-[3px] border-transparent hover:shadow-md hover:scale-[1.01] hover:border-l-teal/30 transition-all duration-300"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<h3 className="font-primary text-[17px] font-semibold text-heading leading-tight">
|
||||
{experience.role}
|
||||
</h3>
|
||||
<p className="font-primary text-sm text-teal mt-0.5">{experience.org}</p>
|
||||
<span className="inline-block px-2.5 py-0.5 mt-1.5 mb-3 bg-teal/8 rounded-full font-secondary text-xs text-teal font-medium">
|
||||
{experience.date}
|
||||
</span>
|
||||
<ul className="space-y-1">
|
||||
{experience.bullets.map((bullet, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="relative pl-4 text-sm text-text leading-relaxed before:content-[''] before:absolute before:left-0 before:top-[10px] before:w-[5px] before:h-[5px] before:rounded-full before:bg-teal"
|
||||
>
|
||||
{bullet}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Experience() {
|
||||
const [sectionRef, isVisible] = useScrollReveal<HTMLDivElement>({ threshold: 0.1 })
|
||||
|
||||
return (
|
||||
<div
|
||||
id="experience"
|
||||
ref={sectionRef}
|
||||
className="py-20 opacity-0 translate-y-6 transition-all duration-600 ease-out data-[visible=true]:opacity-100 data-[visible=true]:translate-y-0"
|
||||
data-visible={isVisible}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-4 mb-8">
|
||||
<h2 className="font-primary text-2xl font-bold text-heading">Experience</h2>
|
||||
<ECGDecoration />
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute left-[20%] top-0 bottom-0 w-0.5 bg-teal/20 hidden md:block" />
|
||||
|
||||
<div className="space-y-0">
|
||||
{experiences.map((exp, i) => (
|
||||
<TimelineEntry
|
||||
key={exp.role}
|
||||
experience={exp}
|
||||
index={i}
|
||||
isVisible={isVisible}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,9 @@ interface UseScrollRevealOptions {
|
||||
|
||||
export function useScrollReveal<T extends HTMLElement>(
|
||||
options: UseScrollRevealOptions = {}
|
||||
): [RefObject<T | null>, boolean] {
|
||||
): [RefObject<T>, boolean] {
|
||||
const { threshold = 0.15, rootMargin = '0px', triggerOnce = true } = options
|
||||
const ref = useRef<T | null>(null)
|
||||
const ref = useRef<T>(null)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user