feat: Build Education, Projects, and Contact sections (Task 9)
This commit is contained in:
+6
-18
@@ -6,6 +6,9 @@ import { FloatingNav } from './components/FloatingNav'
|
|||||||
import { Hero } from './components/Hero'
|
import { Hero } from './components/Hero'
|
||||||
import { Skills } from './components/Skills'
|
import { Skills } from './components/Skills'
|
||||||
import { Experience } from './components/Experience'
|
import { Experience } from './components/Experience'
|
||||||
|
import { Education } from './components/Education'
|
||||||
|
import { Projects } from './components/Projects'
|
||||||
|
import { Contact } from './components/Contact'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [phase, setPhase] = useState<Phase>('boot')
|
const [phase, setPhase] = useState<Phase>('boot')
|
||||||
@@ -30,26 +33,11 @@ function App() {
|
|||||||
|
|
||||||
<Experience />
|
<Experience />
|
||||||
|
|
||||||
<section id="education" className="py-20">
|
<Education />
|
||||||
<h2 className="font-primary text-2xl font-bold text-heading text-center mb-8">
|
|
||||||
Education
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted text-center">Education section will be built in Task 9</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="projects" className="py-20">
|
<Projects />
|
||||||
<h2 className="font-primary text-2xl font-bold text-heading text-center mb-8">
|
|
||||||
Projects
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted text-center">Projects section will be built in Task 9</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="contact" className="py-20">
|
<Contact />
|
||||||
<h2 className="font-primary text-2xl font-bold text-heading text-center mb-8">
|
|
||||||
Contact
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted text-center">Contact section will be built in Task 9</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Phone, Mail, Linkedin, MapPin } from 'lucide-react'
|
||||||
|
import { useScrollReveal } from '@/hooks/useScrollReveal'
|
||||||
|
import type { ContactItem } from '@/types'
|
||||||
|
|
||||||
|
const contactData: ContactItem[] = [
|
||||||
|
{
|
||||||
|
icon: 'phone',
|
||||||
|
value: '07795553088',
|
||||||
|
label: 'Phone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mail',
|
||||||
|
value: 'andy@charlwood.xyz',
|
||||||
|
label: 'Email',
|
||||||
|
href: 'mailto:andy@charlwood.xyz',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'linkedin',
|
||||||
|
value: 'linkedin.com/in/andrewcharlwood',
|
||||||
|
label: 'LinkedIn',
|
||||||
|
href: 'https://linkedin.com/in/andrewcharlwood',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mapPin',
|
||||||
|
value: 'Norwich, UK',
|
||||||
|
label: 'Location',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
phone: Phone,
|
||||||
|
mail: Mail,
|
||||||
|
linkedin: Linkedin,
|
||||||
|
mapPin: MapPin,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContactItemCard = ({
|
||||||
|
item,
|
||||||
|
delay,
|
||||||
|
isVisible,
|
||||||
|
}: {
|
||||||
|
item: ContactItem
|
||||||
|
delay: number
|
||||||
|
isVisible: boolean
|
||||||
|
}) => {
|
||||||
|
const Icon = iconMap[item.icon]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
|
||||||
|
transition={{ duration: 0.5, delay, ease: 'easeOut' }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-full bg-[rgba(0,137,123,0.08)] flex items-center justify-center mx-auto mb-2 text-teal">
|
||||||
|
<Icon size={18} />
|
||||||
|
</div>
|
||||||
|
<div className="font-secondary text-[13px] text-heading break-words">
|
||||||
|
{item.href ? (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
target={item.href.startsWith('http') ? '_blank' : undefined}
|
||||||
|
rel={item.href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||||
|
className="text-teal hover:text-[#00796B] transition-colors"
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
item.value
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="font-secondary text-[10px] uppercase tracking-wider text-muted mt-0.5">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Contact() {
|
||||||
|
const [sectionRef, isVisible] = useScrollReveal<HTMLElement>({
|
||||||
|
threshold: 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="contact" ref={sectionRef} className="py-20">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="font-primary text-2xl font-bold text-heading text-center mb-8"
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{contactData.map((item, index) => (
|
||||||
|
<ContactItemCard
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
delay={0.1 + index * 0.1}
|
||||||
|
isVisible={isVisible}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useScrollReveal } from '@/hooks/useScrollReveal'
|
||||||
|
import type { Education as EducationType } from '@/types'
|
||||||
|
|
||||||
|
const educationData: EducationType[] = [
|
||||||
|
{
|
||||||
|
degree: 'MPharm (Hons) Pharmacy',
|
||||||
|
institution: 'University of East Anglia',
|
||||||
|
period: '2011 — 2015',
|
||||||
|
detail: 'Upper Second-Class Honours (2:1)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
degree: 'Mary Seacole Leadership Programme',
|
||||||
|
institution: 'NHS Leadership Academy',
|
||||||
|
period: '2018',
|
||||||
|
detail: 'National healthcare leadership development programme.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const EducationCard = ({
|
||||||
|
education,
|
||||||
|
delay,
|
||||||
|
isVisible,
|
||||||
|
}: {
|
||||||
|
education: EducationType
|
||||||
|
delay: number
|
||||||
|
isVisible: boolean
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
|
||||||
|
transition={{ duration: 0.5, delay, ease: 'easeOut' }}
|
||||||
|
className="relative bg-white rounded-2xl p-6 shadow-sm overflow-hidden transition-shadow hover:shadow-md hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-teal to-coral" />
|
||||||
|
<h3 className="font-primary text-[17px] font-semibold text-heading leading-tight">
|
||||||
|
{education.degree}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-teal mt-0.5">{education.institution}</p>
|
||||||
|
<p className="text-[13px] text-muted mt-0.5">{education.period}</p>
|
||||||
|
<p className="text-sm text-text mt-1.5 leading-relaxed">
|
||||||
|
{education.detail}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Education() {
|
||||||
|
const [sectionRef, isVisible] = useScrollReveal<HTMLElement>({
|
||||||
|
threshold: 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="education" ref={sectionRef} className="py-20">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="font-primary text-2xl font-bold text-heading text-center mb-8"
|
||||||
|
>
|
||||||
|
Education
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{educationData.map((education, index) => (
|
||||||
|
<EducationCard
|
||||||
|
key={education.degree}
|
||||||
|
education={education}
|
||||||
|
delay={0.1 + index * 0.1}
|
||||||
|
isVisible={isVisible}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={isVisible ? { opacity: 1 } : { opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.4 }}
|
||||||
|
className="text-[13px] text-muted text-center mt-5"
|
||||||
|
>
|
||||||
|
A-Levels: Mathematics (A*), Chemistry (B), Politics (C)
|
||||||
|
</motion.p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ExternalLink } from 'lucide-react'
|
||||||
|
import { useScrollReveal } from '@/hooks/useScrollReveal'
|
||||||
|
import type { Project as ProjectType } from '@/types'
|
||||||
|
|
||||||
|
const projectsData: ProjectType[] = [
|
||||||
|
{
|
||||||
|
title: 'PharMetrics',
|
||||||
|
description:
|
||||||
|
'Real-time medicines expenditure dashboard providing actionable analytics for NHS decision-makers.',
|
||||||
|
link: 'https://medicines.charlwood.xyz/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Patient Pathway Analysis',
|
||||||
|
description:
|
||||||
|
'Data-driven analysis of patient pathways to identify optimisation opportunities and improve clinical outcomes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Blueteq Generator',
|
||||||
|
description:
|
||||||
|
'Automation tool reducing high-cost drug approval processing time by 70%, saving 200+ hours annually.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NMS Video',
|
||||||
|
description:
|
||||||
|
'Educational video resource supporting New Medicine Service consultations, improving patient engagement.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const ProjectCard = ({
|
||||||
|
project,
|
||||||
|
delay,
|
||||||
|
isVisible,
|
||||||
|
}: {
|
||||||
|
project: ProjectType
|
||||||
|
delay: number
|
||||||
|
isVisible: boolean
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
|
||||||
|
transition={{ duration: 0.5, delay, ease: 'easeOut' }}
|
||||||
|
className="group relative bg-white rounded-2xl p-6 shadow-sm overflow-hidden transition-all hover:shadow-md hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-2xl p-[2px] opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #00897B, #FF6B6B)',
|
||||||
|
WebkitMask:
|
||||||
|
'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||||
|
WebkitMaskComposite: 'xor',
|
||||||
|
maskComposite: 'exclude',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<h3 className="font-primary text-base font-semibold text-heading leading-tight">
|
||||||
|
{project.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-text leading-relaxed mt-2">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
{project.link && (
|
||||||
|
<a
|
||||||
|
href={project.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 mt-3 px-4 py-1.5 bg-teal text-white rounded-full text-xs font-medium font-secondary transition-all hover:bg-[#00796B] hover:-translate-y-px"
|
||||||
|
>
|
||||||
|
Visit Project
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Projects() {
|
||||||
|
const [sectionRef, isVisible] = useScrollReveal<HTMLElement>({
|
||||||
|
threshold: 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="projects" ref={sectionRef} className="py-20">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="font-primary text-2xl font-bold text-heading text-center mb-8"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{projectsData.map((project, index) => (
|
||||||
|
<ProjectCard
|
||||||
|
key={project.title}
|
||||||
|
project={project}
|
||||||
|
delay={0.1 + index * 0.1}
|
||||||
|
isVisible={isVisible}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user