Files
portfolio/src/components/Contact.tsx
T
admin 6cc54d8a29 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
2026-02-10 17:20:27 +00:00

109 lines
2.7 KiB
TypeScript

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: 24 }}
animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
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-12 xs:py-16 md: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>
)
}