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' && (
<>
<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 />
<Skills />
+3 -3
View File
@@ -48,8 +48,8 @@ const ContactItemCard = ({
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay, ease: 'easeOut' }}
className="text-center"
>
@@ -83,7 +83,7 @@ export function Contact() {
})
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
initial={{ 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 (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
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"
>
@@ -52,7 +52,7 @@ export function Education() {
})
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
initial={{ opacity: 0, y: 12 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
@@ -62,7 +62,7 @@ export function Education() {
Education
</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) => (
<EducationCard
key={education.degree}
+5 -5
View File
@@ -66,7 +66,7 @@ const experiences: ExperienceType[] = [
const ECGDecoration = () => (
<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"
fill="none"
aria-hidden="true"
@@ -94,8 +94,8 @@ const TimelineEntry = ({
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 }}
initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<div
@@ -104,7 +104,7 @@ const TimelineEntry = ({
}`}
/>
<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 }}
transition={{ duration: 0.2 }}
>
@@ -137,7 +137,7 @@ export function Experience() {
<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"
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}
>
<div className="flex items-center justify-center gap-4 mb-8">
+2 -2
View File
@@ -28,7 +28,7 @@ export function FloatingNav() {
return (
<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 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
@@ -41,7 +41,7 @@ export function FloatingNav() {
key={link.id}
onClick={() => scrollToSection(link.id)}
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
${isActive
? 'text-teal font-semibold'
+1 -1
View File
@@ -7,7 +7,7 @@ const Footer: React.FC = () => {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
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
className="block mx-auto mb-3"
+4 -4
View File
@@ -35,14 +35,14 @@ export function Hero() {
return (
<section
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
initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className="font-primary font-bold text-heading leading-tight"
style={{ fontSize: 'clamp(36px, 5vw, 52px)' }}
style={{ fontSize: 'clamp(28px, 5vw, 52px)' }}
>
Andy Charlwood
</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.
</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="Python/SQL/BI" label="Analytics Stack" valueSize="small" delay={0.5} />
<VitalCard value="Pop. Health" label="Focus Area" valueSize="medium" delay={0.6} />
+4 -4
View File
@@ -38,8 +38,8 @@ const ProjectCard = ({
}) => {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
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"
>
@@ -80,7 +80,7 @@ export function Projects() {
})
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
initial={{ opacity: 0, y: 12 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
@@ -90,7 +90,7 @@ export function Projects() {
Projects
</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) => (
<ProjectCard
key={project.title}
+6 -8
View File
@@ -28,15 +28,13 @@ function SkillGauge({ skill, delay, isVisible }: SkillGaugeProps) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
initial={{ opacity: 0, y: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
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
className="skill-gauge block"
width="80"
height="80"
className="skill-gauge block w-16 h-16 xs:w-20 xs:h-20"
viewBox="0 0 80 80"
>
<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">
{label}
</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) => (
<SkillGauge
key={skill.name}
@@ -162,7 +160,7 @@ export function Skills() {
const strategicSkills = skillsData.filter(s => s.category === 'Strategic')
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
initial={{ opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}