Initial commit: iOS developer portfolio with bio-digital aesthetic
Features: - Hero section with animated circuit traces and botanical SVG decorations - Pulsing circuit animations with fading comet tails connecting plant elements - Mobile-focused About section highlighting Swift and Flutter expertise - Technologies & Skills showcase with properly padded badges - Contact form with validation and EmailJS integration - Responsive design with Tailwind CSS v4 and custom sage/charcoal color palette - Smooth scroll animations and fade-in effects - Centered layout with explicit inline styles for Tailwind v4 compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
21
src/App.jsx
Normal file
21
src/App.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import Header from './components/layout/Header';
|
||||
import Footer from './components/layout/Footer';
|
||||
import Hero from './components/sections/Hero';
|
||||
import About from './components/sections/About';
|
||||
import Contact from './components/sections/Contact';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="min-h-screen w-full overflow-x-hidden">
|
||||
<Header />
|
||||
<main className="w-full">
|
||||
<Hero />
|
||||
<About />
|
||||
<Contact />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
57
src/components/layout/Footer.jsx
Normal file
57
src/components/layout/Footer.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Github, Linkedin, Mail, ArrowUp } from 'lucide-react';
|
||||
|
||||
const Footer = () => {
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const socialLinks = [
|
||||
{ icon: Github, href: '#', label: 'GitHub' },
|
||||
{ icon: Linkedin, href: '#', label: 'LinkedIn' },
|
||||
{ icon: Mail, href: '#contact', label: 'Email' },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-white border-t border-sage-200">
|
||||
<div className="max-w-6xl mx-auto px-6 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{/* Social Links */}
|
||||
<div className="flex items-center gap-4">
|
||||
{socialLinks.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-charcoal-600 hover:text-sage-600 transition-colors"
|
||||
aria-label={link.label}
|
||||
target={link.href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={link.href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<p className="text-charcoal-600 text-sm">
|
||||
© {new Date().getFullYear()} All rights reserved
|
||||
</p>
|
||||
|
||||
{/* Back to Top */}
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className="text-charcoal-600 hover:text-sage-600 transition-colors flex items-center gap-2"
|
||||
aria-label="Back to top"
|
||||
>
|
||||
<span className="text-sm">Back to top</span>
|
||||
<ArrowUp size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
69
src/components/layout/Header.jsx
Normal file
69
src/components/layout/Header.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useState } from 'react';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
const Header = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const scrollToSection = (sectionId) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Home', id: 'hero' },
|
||||
{ label: 'About', id: 'about' },
|
||||
{ label: 'Contact', id: 'contact' },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 bg-white/80 backdrop-blur-md shadow-sm z-50">
|
||||
<nav className="max-w-6xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-center md:justify-center">
|
||||
{/* Desktop Navigation */}
|
||||
<ul className="hidden md:flex items-center gap-8">
|
||||
{navItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className="text-charcoal-700 hover:text-sage-600 transition-colors font-medium"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="md:hidden absolute right-6 text-charcoal-700 hover:text-sage-600 transition-colors"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{isMenuOpen && (
|
||||
<ul className="md:hidden mt-4 py-4 space-y-4 border-t border-sage-200">
|
||||
{navItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className="block w-full text-left text-charcoal-700 hover:text-sage-600 transition-colors font-medium"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
135
src/components/sections/About.jsx
Normal file
135
src/components/sections/About.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Code, Smartphone, Zap, Globe } from 'lucide-react';
|
||||
|
||||
const About = () => {
|
||||
const skills = [
|
||||
'Swift',
|
||||
'Flutter',
|
||||
'Dart',
|
||||
'iOS Development',
|
||||
'UIKit',
|
||||
'SwiftUI',
|
||||
'Xcode',
|
||||
'Firebase',
|
||||
'REST APIs',
|
||||
'Git',
|
||||
'Core Data',
|
||||
'Cross-platform Development',
|
||||
];
|
||||
|
||||
const highlights = [
|
||||
{
|
||||
icon: Code,
|
||||
title: 'Clean Code',
|
||||
description: 'Writing maintainable, scalable, and efficient code',
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Fast Learner',
|
||||
description: 'Quickly adapting to new technologies and frameworks',
|
||||
},
|
||||
{
|
||||
icon: Smartphone,
|
||||
title: 'Mobile Specialist',
|
||||
description: 'iOS expertise with Swift and cross-platform Flutter development',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'Collaborative',
|
||||
description: 'Working effectively in team environments',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="about" className="py-20 bg-gradient-to-b from-sage-50 via-white to-white relative w-full">
|
||||
{/* Decorative top wave transition */}
|
||||
<div className="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-white/0 to-white/100 -mt-24 pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-6" style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16 opacity-0 animate-fadeInUp">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-charcoal-900 mb-4">
|
||||
About Me
|
||||
</h2>
|
||||
<div className="w-20 h-1 bg-sage-600 mb-8" style={{marginLeft: 'auto', marginRight: 'auto'}}></div>
|
||||
<p className="text-lg text-charcoal-600 max-w-2xl" style={{marginLeft: 'auto', marginRight: 'auto', textAlign: 'center'}}>
|
||||
I'm an iOS developer passionate about creating innovative mobile experiences and building
|
||||
user-friendly applications. With expertise in Swift and Flutter, I specialize in crafting
|
||||
elegant native and cross-platform solutions. I enjoy tackling challenging problems and
|
||||
continuously learning new technologies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Highlights Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
|
||||
{highlights.map((highlight, index) => {
|
||||
const Icon = highlight.icon;
|
||||
const delayClass = `delay-${(index + 1) * 100}`;
|
||||
return (
|
||||
<div
|
||||
key={highlight.title}
|
||||
className={`bg-sage-50 rounded-xl p-6 hover:shadow-lg transition-shadow border border-sage-100 opacity-0 animate-fadeInUp ${delayClass}`}
|
||||
>
|
||||
<div className="w-12 h-12 bg-sage-200 rounded-lg flex items-center justify-center mb-4">
|
||||
<Icon className="text-sage-700" size={24} />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-charcoal-900 mb-2">
|
||||
{highlight.title}
|
||||
</h3>
|
||||
<p className="text-sm text-charcoal-600">
|
||||
{highlight.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Skills Section */}
|
||||
<div className="bg-gradient-to-br from-sage-50 to-white rounded-2xl p-8 md:p-12 opacity-0 animate-fadeIn delay-400">
|
||||
<h3 className="text-2xl font-bold text-charcoal-900 mb-6 text-center">
|
||||
Technologies & Skills
|
||||
</h3>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{skills.map((skill) => (
|
||||
<span
|
||||
key={skill}
|
||||
className="py-4 bg-white text-charcoal-700 rounded-lg font-medium shadow-sm hover:shadow-md transition-shadow border border-sage-200"
|
||||
style={{paddingLeft: '3rem', paddingRight: '3rem'}}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume Download */}
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href="#"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-sage-600 text-white rounded-lg font-medium hover:bg-sage-700 transition-colors shadow-md hover:shadow-lg"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Download Resume
|
||||
</a>
|
||||
<p className="text-sm text-charcoal-500 mt-2">
|
||||
{/* TODO: Add your resume link */}
|
||||
(Link your resume PDF here)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
206
src/components/sections/Contact.jsx
Normal file
206
src/components/sections/Contact.jsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import emailjs from '@emailjs/browser';
|
||||
import { Mail, User, MessageSquare, Send, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
|
||||
const Contact = () => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState(null); // 'success' | 'error' | null
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus(null);
|
||||
|
||||
try {
|
||||
// TODO: Set up EmailJS account and replace these values
|
||||
// 1. Go to https://www.emailjs.com/ and create a free account
|
||||
// 2. Add an email service (Gmail, Outlook, etc.)
|
||||
// 3. Create an email template
|
||||
// 4. Replace the values below with your actual IDs
|
||||
|
||||
const EMAILJS_SERVICE_ID = 'YOUR_SERVICE_ID';
|
||||
const EMAILJS_TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
|
||||
const EMAILJS_PUBLIC_KEY = 'YOUR_PUBLIC_KEY';
|
||||
|
||||
await emailjs.send(
|
||||
EMAILJS_SERVICE_ID,
|
||||
EMAILJS_TEMPLATE_ID,
|
||||
{
|
||||
from_name: data.name,
|
||||
from_email: data.email,
|
||||
message: data.message,
|
||||
},
|
||||
EMAILJS_PUBLIC_KEY
|
||||
);
|
||||
|
||||
setSubmitStatus('success');
|
||||
reset();
|
||||
} catch (error) {
|
||||
console.error('Failed to send email:', error);
|
||||
setSubmitStatus('error');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-20 bg-gradient-to-br from-sage-50 to-white w-full">
|
||||
<div className="max-w-4xl mx-auto px-6" style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-charcoal-900 mb-4">
|
||||
Get In Touch
|
||||
</h2>
|
||||
<div className="w-20 h-1 bg-sage-600 mx-auto mb-8"></div>
|
||||
<p className="text-lg text-charcoal-600 max-w-2xl mx-auto">
|
||||
Have a question or want to work together? Feel free to reach out!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 md:p-12">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-charcoal-700 mb-2">
|
||||
<User className="inline-block w-4 h-4 mr-2" />
|
||||
Your Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register('name', {
|
||||
required: 'Name is required',
|
||||
minLength: { value: 2, message: 'Name must be at least 2 characters' }
|
||||
})}
|
||||
className={`w-full px-4 py-3 rounded-lg border ${
|
||||
errors.name ? 'border-red-500' : 'border-sage-300'
|
||||
} focus:outline-none focus:ring-2 focus:ring-sage-500 transition-colors`}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600 flex items-center gap-1">
|
||||
<AlertCircle size={14} />
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-charcoal-700 mb-2">
|
||||
<Mail className="inline-block w-4 h-4 mr-2" />
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address',
|
||||
},
|
||||
})}
|
||||
className={`w-full px-4 py-3 rounded-lg border ${
|
||||
errors.email ? 'border-red-500' : 'border-sage-300'
|
||||
} focus:outline-none focus:ring-2 focus:ring-sage-500 transition-colors`}
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-600 flex items-center gap-1">
|
||||
<AlertCircle size={14} />
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message Field */}
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-charcoal-700 mb-2">
|
||||
<MessageSquare className="inline-block w-4 h-4 mr-2" />
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={6}
|
||||
{...register('message', {
|
||||
required: 'Message is required',
|
||||
minLength: { value: 10, message: 'Message must be at least 10 characters' },
|
||||
})}
|
||||
className={`w-full px-4 py-3 rounded-lg border ${
|
||||
errors.message ? 'border-red-500' : 'border-sage-300'
|
||||
} focus:outline-none focus:ring-2 focus:ring-sage-500 transition-colors resize-none`}
|
||||
placeholder="Tell me about your project or just say hi!"
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="mt-1 text-sm text-red-600 flex items-center gap-1">
|
||||
<AlertCircle size={14} />
|
||||
{errors.message.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full px-8 py-4 bg-sage-600 text-white rounded-lg font-medium hover:bg-sage-700 transition-colors shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send size={20} />
|
||||
Send Message
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Status Messages */}
|
||||
{submitStatus === 'success' && (
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-lg flex items-center gap-2 text-green-800">
|
||||
<CheckCircle size={20} />
|
||||
<p>Message sent successfully! I'll get back to you soon.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{submitStatus === 'error' && (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2 text-red-800">
|
||||
<AlertCircle size={20} />
|
||||
<p>Failed to send message. Please try again or contact me directly at [your-email@example.com]</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{/* Alternative Contact Info */}
|
||||
<div className="mt-8 pt-8 border-t border-sage-200">
|
||||
<p className="text-center text-charcoal-600 mb-4">
|
||||
Or reach out directly:
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 text-sm">
|
||||
<a
|
||||
href="mailto:your-email@example.com"
|
||||
className="flex items-center gap-2 text-sage-600 hover:text-sage-700 transition-colors"
|
||||
>
|
||||
<Mail size={16} />
|
||||
your-email@example.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
375
src/components/sections/Hero.jsx
Normal file
375
src/components/sections/Hero.jsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import { ArrowDown } from 'lucide-react';
|
||||
|
||||
const Hero = () => {
|
||||
const scrollToAbout = () => {
|
||||
document.getElementById('about')?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const scrollToContact = () => {
|
||||
document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="hero" className="min-h-screen flex items-center justify-center relative bg-gradient-to-br from-sage-50 to-white overflow-hidden">
|
||||
{/* Animated circuit connections */}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" viewBox="0 0 1920 1080">
|
||||
<defs>
|
||||
{/* Radial gradients for pulse glow effect */}
|
||||
<radialGradient id="pulseGlow1">
|
||||
<stop offset="0%" stopColor="#7fb069" stopOpacity="1"/>
|
||||
<stop offset="50%" stopColor="#7fb069" stopOpacity="0.6"/>
|
||||
<stop offset="100%" stopColor="#7fb069" stopOpacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="pulseGlow2">
|
||||
<stop offset="0%" stopColor="#5a9650" stopOpacity="1"/>
|
||||
<stop offset="50%" stopColor="#5a9650" stopOpacity="0.6"/>
|
||||
<stop offset="100%" stopColor="#5a9650" stopOpacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
{/* Circuit pulse animation */}
|
||||
<style>
|
||||
{`
|
||||
@keyframes pulse-node {
|
||||
0%, 100% { opacity: 0.3; r: 6; }
|
||||
50% { opacity: 0.8; r: 10; }
|
||||
}
|
||||
.circuit-node {
|
||||
animation: pulse-node 2s ease-in-out infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
{/* Solid circuit paths */}
|
||||
{/* Path 1: Top left to middle left */}
|
||||
<path id="path1" d="M 90 90 L 90 300 L 160 300 L 160 540"
|
||||
stroke="#7fb069"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.3" />
|
||||
|
||||
{/* Path 2: Middle left to bottom left */}
|
||||
<path id="path2" d="M 160 540 L 160 850 L 90 850"
|
||||
stroke="#5a9650"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.3" />
|
||||
|
||||
{/* Path 3: Bottom left to bottom right */}
|
||||
<path id="path3" d="M 90 850 L 90 950 L 1800 950 L 1800 900"
|
||||
stroke="#5a9650"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.3" />
|
||||
|
||||
{/* Path 4: Bottom right to middle right */}
|
||||
<path id="path4" d="M 1800 900 L 1830 900 L 1830 360 L 1760 360"
|
||||
stroke="#4a7c42"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.3" />
|
||||
|
||||
{/* Path 5: Middle right to top right */}
|
||||
<path id="path5" d="M 1760 360 L 1776 360 L 1776 180"
|
||||
stroke="#7fb069"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.3" />
|
||||
|
||||
{/* Path 6: Top right to top left */}
|
||||
<path id="path6" d="M 1776 180 L 1776 40 L 90 40 L 90 90"
|
||||
stroke="#7fb069"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.3" />
|
||||
|
||||
{/* Additional center-crossing circuit traces */}
|
||||
{/* Horizontal center traces */}
|
||||
<path d="M 300 540 L 1620 540"
|
||||
stroke="#7fb069"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.2" />
|
||||
|
||||
<path d="M 400 700 L 1520 700"
|
||||
stroke="#5a9650"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.2" />
|
||||
|
||||
<path d="M 350 380 L 1570 380"
|
||||
stroke="#4a7c42"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.2" />
|
||||
|
||||
{/* Vertical center traces */}
|
||||
<path d="M 960 200 L 960 880"
|
||||
stroke="#7fb069"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.2" />
|
||||
|
||||
{/* Diagonal traces */}
|
||||
<path d="M 400 300 L 700 500"
|
||||
stroke="#5a9650"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.15" />
|
||||
|
||||
<path d="M 1220 500 L 1520 300"
|
||||
stroke="#5a9650"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.15" />
|
||||
|
||||
<path d="M 500 780 L 800 600"
|
||||
stroke="#4a7c42"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.15" />
|
||||
|
||||
<path d="M 1120 600 L 1420 780"
|
||||
stroke="#4a7c42"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
opacity="0.15" />
|
||||
|
||||
{/* Small circuit nodes at intersections */}
|
||||
<circle cx="960" cy="540" r="3" fill="#7fb069" opacity="0.4" />
|
||||
<circle cx="400" cy="700" r="3" fill="#5a9650" opacity="0.4" />
|
||||
<circle cx="1520" cy="700" r="3" fill="#5a9650" opacity="0.4" />
|
||||
<circle cx="350" cy="380" r="3" fill="#4a7c42" opacity="0.4" />
|
||||
<circle cx="1570" cy="380" r="3" fill="#4a7c42" opacity="0.4" />
|
||||
<circle cx="960" cy="200" r="3" fill="#7fb069" opacity="0.4" />
|
||||
<circle cx="960" cy="880" r="3" fill="#7fb069" opacity="0.4" />
|
||||
<circle cx="700" cy="500" r="3" fill="#5a9650" opacity="0.4" />
|
||||
<circle cx="1220" cy="500" r="3" fill="#5a9650" opacity="0.4" />
|
||||
|
||||
{/* Complete circuit path for continuous pulse */}
|
||||
<path id="fullCircuit" d="M 90 90 L 90 300 L 160 300 L 160 540 L 160 850 L 90 850 L 90 950 L 1800 950 L 1800 900 L 1830 900 L 1830 360 L 1760 360 L 1776 360 L 1776 180 L 1776 40 L 90 40 L 90 90"
|
||||
fill="none"
|
||||
opacity="0"/>
|
||||
|
||||
{/* Animated pulse with tails - First pulse */}
|
||||
{/* Tail segments (fading trail behind main pulse) */}
|
||||
<circle r="14" fill="url(#pulseGlow1)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="0.15s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;0.3;0.3;0.3;0.3;0.3;0.3;0.3;0.3;0" dur="12s" repeatCount="indefinite" begin="0.15s"/>
|
||||
</circle>
|
||||
|
||||
<circle r="16" fill="url(#pulseGlow1)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="0.3s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;0.2;0.2;0.2;0.2;0.2;0.2;0.2;0.2;0" dur="12s" repeatCount="indefinite" begin="0.3s"/>
|
||||
</circle>
|
||||
|
||||
<circle r="18" fill="url(#pulseGlow1)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="0.45s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;0.1;0.1;0.1;0.1;0.1;0.1;0.1;0.1;0" dur="12s" repeatCount="indefinite" begin="0.45s"/>
|
||||
</circle>
|
||||
|
||||
{/* Main pulse head */}
|
||||
<circle r="12" fill="url(#pulseGlow1)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="0s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;1;1;1;1;1;1;1;1;0" dur="12s" repeatCount="indefinite" begin="0s"/>
|
||||
</circle>
|
||||
|
||||
{/* Animated pulse with tails - Second pulse */}
|
||||
{/* Tail segments */}
|
||||
<circle r="14" fill="url(#pulseGlow2)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="6.15s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;0.3;0.3;0.3;0.3;0.3;0.3;0.3;0.3;0" dur="12s" repeatCount="indefinite" begin="6.15s"/>
|
||||
</circle>
|
||||
|
||||
<circle r="16" fill="url(#pulseGlow2)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="6.3s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;0.2;0.2;0.2;0.2;0.2;0.2;0.2;0.2;0" dur="12s" repeatCount="indefinite" begin="6.3s"/>
|
||||
</circle>
|
||||
|
||||
<circle r="18" fill="url(#pulseGlow2)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="6.45s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;0.1;0.1;0.1;0.1;0.1;0.1;0.1;0.1;0" dur="12s" repeatCount="indefinite" begin="6.45s"/>
|
||||
</circle>
|
||||
|
||||
{/* Main pulse head */}
|
||||
<circle r="12" fill="url(#pulseGlow2)" opacity="0">
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="6s">
|
||||
<mpath href="#fullCircuit"/>
|
||||
</animateMotion>
|
||||
<animate attributeName="opacity" values="0;1;1;1;1;1;1;1;1;0" dur="12s" repeatCount="indefinite" begin="6s"/>
|
||||
</circle>
|
||||
|
||||
{/* Static circuit nodes at plant centers */}
|
||||
<circle cx="90" cy="90" r="6" fill="#7fb069" className="circuit-node" />
|
||||
<circle cx="1776" cy="180" r="6" fill="#7fb069" className="circuit-node" style={{animationDelay: '0.3s'}} />
|
||||
<circle cx="1800" cy="900" r="6" fill="#5a9650" className="circuit-node" style={{animationDelay: '0.6s'}} />
|
||||
<circle cx="90" cy="850" r="6" fill="#5a9650" className="circuit-node" style={{animationDelay: '0.9s'}} />
|
||||
<circle cx="160" cy="540" r="6" fill="#4a7c42" className="circuit-node" style={{animationDelay: '1.2s'}} />
|
||||
<circle cx="1760" cy="360" r="6" fill="#4a7c42" className="circuit-node" style={{animationDelay: '1.5s'}} />
|
||||
</svg>
|
||||
|
||||
{/* Decorative plant elements */}
|
||||
{/* Top left - stem with branches */}
|
||||
<div className="absolute top-10 left-10 w-32 h-32 opacity-20">
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path
|
||||
d="M100 20C100 20 80 60 80 100C80 140 100 180 100 180"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
className="text-sage-600"
|
||||
/>
|
||||
<path
|
||||
d="M100 50C100 50 130 70 140 100C150 130 130 150 130 150"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-sage-500"
|
||||
/>
|
||||
<path
|
||||
d="M100 50C100 50 70 70 60 100C50 130 70 150 70 150"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-sage-500"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Bottom right - plant with leaves */}
|
||||
<div className="absolute bottom-20 right-20 w-40 h-40 opacity-20">
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<circle cx="100" cy="100" r="5" fill="currentColor" className="text-sage-600" />
|
||||
<path
|
||||
d="M100 95C100 95 110 70 120 50"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-sage-500"
|
||||
/>
|
||||
<ellipse cx="125" cy="40" rx="8" ry="12" fill="currentColor" className="text-sage-400" />
|
||||
<path
|
||||
d="M100 95C100 95 85 70 70 55"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-sage-500"
|
||||
/>
|
||||
<ellipse cx="65" cy="48" rx="7" ry="10" fill="currentColor" className="text-sage-400" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Top right - fern-like plant */}
|
||||
<div className="absolute top-32 right-16 w-36 h-36 opacity-20">
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path
|
||||
d="M100 180C100 180 100 100 100 20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-sage-600"
|
||||
/>
|
||||
<ellipse cx="80" cy="60" rx="20" ry="8" fill="currentColor" className="text-sage-400" />
|
||||
<ellipse cx="120" cy="80" rx="22" ry="9" fill="currentColor" className="text-sage-400" />
|
||||
<ellipse cx="75" cy="100" rx="24" ry="10" fill="currentColor" className="text-sage-500" />
|
||||
<ellipse cx="125" cy="120" rx="26" ry="11" fill="currentColor" className="text-sage-500" />
|
||||
<ellipse cx="70" cy="140" rx="20" ry="8" fill="currentColor" className="text-sage-400" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Bottom left - monstera-style leaf */}
|
||||
<div className="absolute bottom-32 left-16 w-28 h-28 opacity-20">
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path
|
||||
d="M100 160C100 160 60 140 50 100C40 60 70 30 100 40C130 30 160 60 150 100C140 140 100 160 100 160Z"
|
||||
fill="currentColor"
|
||||
className="text-sage-400"
|
||||
/>
|
||||
<path
|
||||
d="M100 160L100 40"
|
||||
stroke="white"
|
||||
strokeWidth="4"
|
||||
className="opacity-50"
|
||||
/>
|
||||
<path
|
||||
d="M80 100C80 100 90 90 100 90C110 90 120 100 120 100"
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
className="opacity-50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Middle left - small sprout */}
|
||||
<div className="absolute top-1/2 left-32 w-20 h-20 opacity-20">
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<path
|
||||
d="M100 150C100 150 100 120 100 80"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-sage-600"
|
||||
/>
|
||||
<ellipse cx="85" cy="70" rx="15" ry="20" fill="currentColor" className="text-sage-500" transform="rotate(-20 85 70)" />
|
||||
<ellipse cx="115" cy="70" rx="15" ry="20" fill="currentColor" className="text-sage-500" transform="rotate(20 115 70)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Middle right - circular leaf pattern */}
|
||||
<div className="absolute top-1/3 right-32 w-24 h-24 opacity-20">
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<circle cx="100" cy="100" r="8" fill="currentColor" className="text-sage-600" />
|
||||
<ellipse cx="100" cy="60" rx="12" ry="25" fill="currentColor" className="text-sage-400" />
|
||||
<ellipse cx="130" cy="85" rx="12" ry="25" fill="currentColor" className="text-sage-400" transform="rotate(60 130 85)" />
|
||||
<ellipse cx="130" cy="115" rx="12" ry="25" fill="currentColor" className="text-sage-400" transform="rotate(120 130 115)" />
|
||||
<ellipse cx="100" cy="140" rx="12" ry="25" fill="currentColor" className="text-sage-400" transform="rotate(180 100 140)" />
|
||||
<ellipse cx="70" cy="115" rx="12" ry="25" fill="currentColor" className="text-sage-400" transform="rotate(240 70 115)" />
|
||||
<ellipse cx="70" cy="85" rx="12" ry="25" fill="currentColor" className="text-sage-400" transform="rotate(300 70 85)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-4xl mx-auto px-6 text-center z-10">
|
||||
<h1 className="text-5xl md:text-7xl font-bold text-charcoal-900 mb-6">
|
||||
Hi, I'm <span className="text-sage-600">Michael Simard</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-charcoal-600 mb-8 max-w-2xl" style={{marginLeft: 'auto', marginRight: 'auto', textAlign: 'center'}}>
|
||||
iOS developer crafting elegant mobile experiences with Swift and Flutter
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12">
|
||||
<button
|
||||
onClick={scrollToAbout}
|
||||
className="py-3 bg-sage-600 text-white rounded-lg font-medium hover:bg-sage-700 transition-colors shadow-md hover:shadow-lg"
|
||||
style={{paddingLeft: '3rem', paddingRight: '3rem'}}
|
||||
>
|
||||
Learn More About Me
|
||||
</button>
|
||||
<button
|
||||
onClick={scrollToContact}
|
||||
className="py-3 bg-white text-sage-600 border-2 border-sage-600 rounded-lg font-medium hover:bg-sage-50 transition-colors"
|
||||
style={{paddingLeft: '3rem', paddingRight: '3rem'}}
|
||||
>
|
||||
Get In Touch
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="animate-bounce">
|
||||
<ArrowDown className="mx-auto text-sage-600" size={32} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
99
src/index.css
Normal file
99
src/index.css
Normal file
@@ -0,0 +1,99 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Custom color palette - plant aesthetic */
|
||||
--color-sage-50: #f5f9f6;
|
||||
--color-sage-100: #e8f3eb;
|
||||
--color-sage-200: #c8e6d0;
|
||||
--color-sage-300: #a4d4b4;
|
||||
--color-sage-400: #7fb069;
|
||||
--color-sage-500: #5a9650;
|
||||
--color-sage-600: #4a7c42;
|
||||
--color-sage-700: #3a6235;
|
||||
--color-sage-800: #2d4d2a;
|
||||
--color-sage-900: #1f3520;
|
||||
|
||||
/* Neutral colors */
|
||||
--color-charcoal-50: #f7f8f8;
|
||||
--color-charcoal-100: #e8eaeb;
|
||||
--color-charcoal-200: #d1d5d8;
|
||||
--color-charcoal-300: #a8b1b7;
|
||||
--color-charcoal-400: #7d8a93;
|
||||
--color-charcoal-500: #636e72;
|
||||
--color-charcoal-600: #525c61;
|
||||
--color-charcoal-700: #444d51;
|
||||
--color-charcoal-800: #3a4145;
|
||||
--color-charcoal-900: #2d3436;
|
||||
|
||||
/* Font family */
|
||||
--font-family-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #fafafa;
|
||||
color: var(--color-charcoal-900);
|
||||
margin: 0 auto;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Scroll animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeInUp {
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Staggered animation delays */
|
||||
.delay-100 {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.delay-200 {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.delay-300 {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.delay-400 {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
10
src/main.jsx
Normal file
10
src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user