Task 11: Implement scroll animations and responsive design

- Add xs (480px) breakpoint to tailwind config for mobile
- Standardize scroll-reveal animations to opacity 0→1, y 24→0
- Add responsive padding to main container (px-5 xs:px-6 md:px-8)
- Add responsive section padding (py-12 xs:py-16 md:py-20)
- FloatingNav: responsive width and font/padding on mobile
- Hero: responsive vitals grid, title font clamp to 28px min
- Skills: responsive grid (2→3→auto-fit), smaller gauges on mobile
- Experience: responsive card padding, ECG decoration size
- Education/Projects: responsive grids matching concept.html
- Contact/Footer: responsive padding
This commit is contained in:
2026-02-10 17:20:27 +00:00
parent 30eff4dde2
commit 6cc54d8a29
10 changed files with 37 additions and 32 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ function App() {
{phase === 'content' && ( {phase === 'content' && (
<> <>
<FloatingNav /> <FloatingNav />
<main className="max-w-[1000px] mx-auto px-8"> <main className="max-w-[1000px] mx-auto px-5 xs:px-6 md:px-8">
<Hero /> <Hero />
<Skills /> <Skills />
+3 -3
View File
@@ -48,8 +48,8 @@ const ContactItemCard = ({
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay, ease: 'easeOut' }} transition={{ duration: 0.5, delay, ease: 'easeOut' }}
className="text-center" className="text-center"
> >
@@ -83,7 +83,7 @@ export function Contact() {
}) })
return ( return (
<section id="contact" ref={sectionRef} className="py-20"> <section id="contact" ref={sectionRef} className="py-12 xs:py-16 md:py-20">
<motion.h2 <motion.h2
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
+4 -4
View File
@@ -28,8 +28,8 @@ const EducationCard = ({
}) => { }) => {
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay, ease: 'easeOut' }} 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" className="relative bg-white rounded-2xl p-6 shadow-sm overflow-hidden transition-shadow hover:shadow-md hover:-translate-y-0.5"
> >
@@ -52,7 +52,7 @@ export function Education() {
}) })
return ( return (
<section id="education" ref={sectionRef} className="py-20"> <section id="education" ref={sectionRef} className="py-12 xs:py-16 md:py-20">
<motion.h2 <motion.h2
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
@@ -62,7 +62,7 @@ export function Education() {
Education Education
</motion.h2> </motion.h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
{educationData.map((education, index) => ( {educationData.map((education, index) => (
<EducationCard <EducationCard
key={education.degree} key={education.degree}
+5 -5
View File
@@ -66,7 +66,7 @@ const experiences: ExperienceType[] = [
const ECGDecoration = () => ( const ECGDecoration = () => (
<svg <svg
className="shrink-0 w-[200px] h-[30px] md:w-[200px] w-[120px]" className="shrink-0 w-[120px] xs:w-[200px] h-[30px]"
viewBox="0 0 200 30" viewBox="0 0 200 30"
fill="none" fill="none"
aria-hidden="true" aria-hidden="true"
@@ -94,8 +94,8 @@ const TimelineEntry = ({
return ( return (
<motion.div <motion.div
className="relative pl-0 md:pl-[calc(20%+32px)] mb-8 last:mb-0" className="relative pl-0 md:pl-[calc(20%+32px)] mb-8 last:mb-0"
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay: index * 0.1 }} transition={{ duration: 0.5, delay: index * 0.1 }}
> >
<div <div
@@ -104,7 +104,7 @@ const TimelineEntry = ({
}`} }`}
/> />
<motion.div <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" className="bg-white rounded-2xl p-4 xs: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 }} whileHover={{ scale: 1.01 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
@@ -137,7 +137,7 @@ export function Experience() {
<div <div
id="experience" id="experience"
ref={sectionRef} 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" className="py-12 xs:py-16 md: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} data-visible={isVisible}
> >
<div className="flex items-center justify-center gap-4 mb-8"> <div className="flex items-center justify-center gap-4 mb-8">
+2 -2
View File
@@ -28,7 +28,7 @@ export function FloatingNav() {
return ( return (
<motion.nav <motion.nav
className="fixed top-4 left-1/2 -translate-x-1/2 z-[100] max-w-[600px] w-auto bg-white rounded-full py-2 px-6 shadow-md flex items-center gap-1 border border-border overflow-x-auto scrollbar-hide" className="fixed top-4 left-1/2 -translate-x-1/2 z-[100] max-w-[600px] w-[calc(100%-32px)] md:w-auto bg-white rounded-full py-2 px-4 md:px-6 shadow-md flex items-center gap-1 border border-border overflow-x-auto scrollbar-hide"
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }} transition={{ duration: 0.5, ease: 'easeOut' }}
@@ -41,7 +41,7 @@ export function FloatingNav() {
key={link.id} key={link.id}
onClick={() => scrollToSection(link.id)} onClick={() => scrollToSection(link.id)}
className={` className={`
relative font-secondary text-[13px] font-medium py-1.5 px-3.5 rounded-full relative font-secondary text-[11px] xs:text-[13px] font-medium py-1.5 px-2.5 xs:px-3.5 rounded-full
transition-colors duration-300 ease-out whitespace-nowrap transition-colors duration-300 ease-out whitespace-nowrap
${isActive ${isActive
? 'text-teal font-semibold' ? 'text-teal font-semibold'
+1 -1
View File
@@ -7,7 +7,7 @@ const Footer: React.FC = () => {
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }} viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, ease: 'easeOut' }} transition={{ duration: 0.5, ease: 'easeOut' }}
className="text-center pt-12 pb-8 border-t border-slate-200" className="text-center pt-8 xs:pt-12 pb-6 xs:pb-8 border-t border-slate-200"
> >
<svg <svg
className="block mx-auto mb-3" className="block mx-auto mb-3"
+4 -4
View File
@@ -35,14 +35,14 @@ export function Hero() {
return ( return (
<section <section
id="about" id="about"
className="min-h-screen flex flex-col justify-center items-center text-center py-20" className="min-h-screen flex flex-col justify-center items-center text-center py-12 xs:py-16 md:py-20"
> >
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }} transition={{ duration: 0.6, ease: 'easeOut' }}
className="font-primary font-bold text-heading leading-tight" className="font-primary font-bold text-heading leading-tight"
style={{ fontSize: 'clamp(36px, 5vw, 52px)' }} style={{ fontSize: 'clamp(28px, 5vw, 52px)' }}
> >
Andy Charlwood Andy Charlwood
</motion.h1> </motion.h1>
@@ -74,7 +74,7 @@ export function Hero() {
GPhC Registered Pharmacist specialising in medicines optimisation, population health analytics, and NHS efficiency programmes. Bridging clinical pharmacy with data science to drive meaningful improvements in patient outcomes. GPhC Registered Pharmacist specialising in medicines optimisation, population health analytics, and NHS efficiency programmes. Bridging clinical pharmacy with data science to drive meaningful improvements in patient outcomes.
</motion.p> </motion.p>
<div className="flex gap-4 mt-10 justify-center flex-wrap"> <div className="grid grid-cols-1 xs:grid-cols-2 md:flex gap-4 mt-10 justify-center md:flex-wrap">
<VitalCard value="10+" label="Years Experience" delay={0.4} /> <VitalCard value="10+" label="Years Experience" delay={0.4} />
<VitalCard value="Python/SQL/BI" label="Analytics Stack" valueSize="small" delay={0.5} /> <VitalCard value="Python/SQL/BI" label="Analytics Stack" valueSize="small" delay={0.5} />
<VitalCard value="Pop. Health" label="Focus Area" valueSize="medium" delay={0.6} /> <VitalCard value="Pop. Health" label="Focus Area" valueSize="medium" delay={0.6} />
+4 -4
View File
@@ -38,8 +38,8 @@ const ProjectCard = ({
}) => { }) => {
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay, ease: 'easeOut' }} 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" className="group relative bg-white rounded-2xl p-6 shadow-sm overflow-hidden transition-all hover:shadow-md hover:-translate-y-0.5"
> >
@@ -80,7 +80,7 @@ export function Projects() {
}) })
return ( return (
<section id="projects" ref={sectionRef} className="py-20"> <section id="projects" ref={sectionRef} className="py-12 xs:py-16 md:py-20">
<motion.h2 <motion.h2
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
@@ -90,7 +90,7 @@ export function Projects() {
Projects Projects
</motion.h2> </motion.h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
{projectsData.map((project, index) => ( {projectsData.map((project, index) => (
<ProjectCard <ProjectCard
key={project.title} key={project.title}
+6 -8
View File
@@ -28,15 +28,13 @@ function SkillGauge({ skill, delay, isVisible }: SkillGaugeProps) {
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay: delay / 1000, ease: 'easeOut' }} transition={{ duration: 0.5, delay: delay / 1000, ease: 'easeOut' }}
className={`flex flex-col items-center p-4 rounded-2xl transition-colors duration-300 ${hoverBg}`} className={`flex flex-col items-center p-3 xs:p-4 rounded-2xl transition-colors duration-300 ${hoverBg}`}
> >
<svg <svg
className="skill-gauge block" className="skill-gauge block w-16 h-16 xs:w-20 xs:h-20"
width="80"
height="80"
viewBox="0 0 80 80" viewBox="0 0 80 80"
> >
<circle <circle
@@ -98,7 +96,7 @@ function SkillCategory({ label, skills, isVisible, baseDelay }: SkillCategoryPro
<h3 className="font-secondary text-xs font-semibold uppercase tracking-widest text-muted mb-5 pl-1"> <h3 className="font-secondary text-xs font-semibold uppercase tracking-widest text-muted mb-5 pl-1">
{label} {label}
</h3> </h3>
<div className="grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-6"> <div className="grid grid-cols-2 xs:grid-cols-3 md:grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-4 xs:gap-6">
{skills.map((skill, index) => ( {skills.map((skill, index) => (
<SkillGauge <SkillGauge
key={skill.name} key={skill.name}
@@ -162,7 +160,7 @@ export function Skills() {
const strategicSkills = skillsData.filter(s => s.category === 'Strategic') const strategicSkills = skillsData.filter(s => s.category === 'Strategic')
return ( return (
<section id="skills" ref={sectionRef} className="py-20"> <section id="skills" ref={sectionRef} className="py-12 xs:py-16 md:py-20">
<motion.h2 <motion.h2
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
+7
View File
@@ -5,6 +5,13 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}",
], ],
theme: { theme: {
screens: {
'xs': '480px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
},
extend: { extend: {
colors: { colors: {
teal: { teal: {