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:
@@ -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 }}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user