feat: Initialize project with Vite and React
Sets up the project structure with Vite, React, and essential libraries like GSAP and Framer Motion. Configures Tailwind CSS for styling, including dark mode and custom color schemes. Includes basic component structure for the application, laying the groundwork for UI development.
This commit is contained in:
parent
4e3516d96a
commit
1a85f0eb2d
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import Lenis from '@studio-freight/lenis';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
||||
import Navbar from './components/Navbar';
|
||||
import Hero from './components/Hero';
|
||||
import Mission from './components/Mission';
|
||||
import Services from './components/Services';
|
||||
import Process from './components/Process';
|
||||
import Features from './components/Features';
|
||||
import Blog from './components/Blog';
|
||||
import Contact from './components/Contact';
|
||||
import Footer from './components/Footer';
|
||||
import BackToTop from './components/BackToTop';
|
||||
|
||||
// Register GSAP plugins globally
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||
|
||||
// Grain Overlay Component
|
||||
const GrainOverlay = () => (
|
||||
<div className="fixed inset-0 w-full h-full pointer-events-none z-50 opacity-50 dark:opacity-100 bg-grain mix-blend-overlay"></div>
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
// Initialize Lenis for smooth scrolling
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
direction: 'vertical',
|
||||
gestureDirection: 'vertical',
|
||||
smooth: true,
|
||||
smoothTouch: false,
|
||||
touchMultiplier: 2,
|
||||
} as any);
|
||||
|
||||
// Synchronize Lenis with GSAP ScrollTrigger
|
||||
lenis.on('scroll', ScrollTrigger.update);
|
||||
|
||||
const ticker = (time: number) => {
|
||||
lenis.raf(time * 1000);
|
||||
};
|
||||
|
||||
// Use GSAP ticker for smoother animation loop integration
|
||||
gsap.ticker.add(ticker);
|
||||
|
||||
// Disable lag smoothing to prevent jumps
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
return () => {
|
||||
gsap.ticker.remove(ticker);
|
||||
lenis.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative w-full min-h-screen bg-background-light dark:bg-background-dark">
|
||||
<GrainOverlay />
|
||||
<Navbar />
|
||||
<main>
|
||||
<Hero />
|
||||
<Mission />
|
||||
<Services />
|
||||
<Process />
|
||||
<Features />
|
||||
<Blog />
|
||||
<Contact />
|
||||
</main>
|
||||
<Footer />
|
||||
<BackToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
README.md
25
README.md
|
|
@ -1,11 +1,20 @@
|
|||
<div align="center">
|
||||
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
|
||||
<h1>Built with AI Studio</h2>
|
||||
|
||||
<p>The fastest path from prompt to production with Gemini.</p>
|
||||
|
||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
||||
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/19QGHBMfoMD7dqVbMKC_4jTa_Dco6mOuv
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import gsap from 'gsap';
|
||||
|
||||
const BackToTop: React.FC = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const toggleVisibility = () => {
|
||||
if (window.scrollY > 500) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', toggleVisibility);
|
||||
return () => window.removeEventListener('scroll', toggleVisibility);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
gsap.to(window, { duration: 1.2, scrollTo: 0, ease: "power3.inOut" });
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
whileHover={{ scale: 1.1, backgroundColor: "#3b82f6" }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={scrollToTop}
|
||||
className="fixed bottom-8 right-8 z-50 w-12 h-12 flex items-center justify-center rounded-full bg-black dark:bg-white text-white dark:text-black shadow-lg border border-gray-700 dark:border-gray-200 transition-colors"
|
||||
aria-label="Back to top"
|
||||
>
|
||||
<span className="material-symbols-outlined text-2xl">arrow_upward</span>
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackToTop;
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import React, { useRef, useLayoutEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const posts = [
|
||||
{
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuARalmRkuoZMBAbavGQgx4a-JhLgXBJ6JSD0U4vycdwaGGV3d-ffUFrdbx2lIbKrYCmS100i7VJ0w5cDHITXYV6w1-pSUPHKL7Jik__TWOIYOnq_4ND5ri7l8SQoaJdjJK9jhYvtxdxrZm6j8t8BNAjvPTaUdUDo4C7QVqcx1KbGvup6cpF8vY1LJ82S_5OMAZ6JgH0rK5bvWpqD3WqPhtqJCUB6d_1gUvluKjotwnNQ03t1dSYV8HOtRrLE83j6i_wgL4GZ0XTsMZb',
|
||||
date: 'Oct 12, 2024',
|
||||
category: 'Cybersecurity',
|
||||
title: 'The Hidden Risks of Remote Work',
|
||||
excerpt: 'As remote work becomes permanent, new vulnerabilities emerge. Learn how to secure your distributed workforce effectively.'
|
||||
},
|
||||
{
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCz5lTYjY4RNXubQlrA-BtLIGR3nUY8ULkD9omwT5FShfdMrbMgS5dDCyfN3xiB5WC7T3vjNvyvVbvnD0G1zBpbNTjfOYyhmAEfno7Hf5W1sm-KYRXYrLGQq-c6TkLgEf0i9JGNvuFZ6edcenr2o39dCzIPXcp_z9XWOIzp7kBX2EydNPLJoRofVYuSTmEA1y0_xh4sdiRy1PykRASGLhKfN19_XLNuwyTBVKYISY7cHc-An69eZpAfhrvngu3E47rU6KuQS0k3QXBZ',
|
||||
date: 'Sep 28, 2024',
|
||||
category: 'Cloud Infrastructure',
|
||||
title: 'Migrating to the Cloud: A Step-by-Step Guide',
|
||||
excerpt: 'Thinking about moving your data? Here is a comprehensive checklist to ensure a smooth and secure transition.'
|
||||
},
|
||||
{
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCl5iOhTsCqcHnho89DkoLh0DYeuvef0pdp8k26NKzcAq7YPvWbAYARg9mCIvqGTxQGradp8zvscuuibskpz4W_nEzQQO1z7lgwKJ1Xxiw_yQOyXMLfoRNLTHXzqFUH8Q5daCAfYTb7Zl3sFjB7k8i44D6TGolzqrN05Db27Abf2TWDDzHpVSrNml4zddvxholHFxMzqDeSzQ5p77SLDSFNaYBZGR2lEdN2V9O0GzMqxbOjFmBGMW48nlrEDLDzYGv_gWI3RSqNqBl-',
|
||||
date: 'Sep 15, 2024',
|
||||
category: 'Innovation',
|
||||
title: 'AI in Business: Beyond the Hype',
|
||||
excerpt: 'Artificial Intelligence is transforming industries. Discover practical applications that can drive efficiency in your business today.'
|
||||
}
|
||||
];
|
||||
|
||||
const Blog: React.FC = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
imagesRef.current = [];
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
imagesRef.current.forEach((imgWrapper) => {
|
||||
if (!imgWrapper) return;
|
||||
|
||||
gsap.to(imgWrapper, {
|
||||
yPercent: 30,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: imgWrapper.closest('article'),
|
||||
start: "top bottom",
|
||||
end: "bottom top",
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}, containerRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
ref={containerRef}
|
||||
id="blog"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="py-24 bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex justify-between items-end mb-12">
|
||||
<div>
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Latest Insights</span>
|
||||
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
||||
Knowledge <span className="text-gray-400 dark:text-gray-600">base.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<motion.a
|
||||
href="#"
|
||||
className="hidden md:inline-flex items-center text-sm font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
View all posts <span className="material-symbols-outlined text-sm ml-1">arrow_forward</span>
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{posts.map((post, i) => (
|
||||
<motion.article
|
||||
key={i}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: i * 0.1 }}
|
||||
whileHover={{ y: -8 }}
|
||||
className="group cursor-pointer"
|
||||
>
|
||||
<div className="h-64 rounded-xl overflow-hidden mb-6 relative shadow-lg">
|
||||
<div
|
||||
ref={el => { if(el) imagesRef.current.push(el); }}
|
||||
className="w-full h-[140%] -mt-[20%]"
|
||||
>
|
||||
<img
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors pointer-events-none"></div>
|
||||
<div className="absolute top-4 right-4 bg-white/90 dark:bg-black/80 backdrop-blur text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wider z-10">
|
||||
Read
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
<span>{post.date}</span>
|
||||
<span className="w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
<span className="text-blue-600 dark:text-blue-400 font-medium">{post.category}</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blog;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const Contact: React.FC = () => {
|
||||
return (
|
||||
<motion.section
|
||||
id="contact"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5"
|
||||
>
|
||||
<div className="max-w-3xl mx-auto px-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="font-display text-4xl md:text-5xl font-medium mb-6 text-gray-900 dark:text-white">
|
||||
Get in Touch
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
||||
Ready to elevate your IT infrastructure? Fill out the form below and we'll get back to you shortly.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.form
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name</label>
|
||||
<motion.input
|
||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
type="text"
|
||||
id="name"
|
||||
placeholder="John Doe"
|
||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email</label>
|
||||
<motion.input
|
||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
type="email"
|
||||
id="email"
|
||||
placeholder="john@company.com"
|
||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message</label>
|
||||
<motion.textarea
|
||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
id="message"
|
||||
placeholder="Tell us about your project..."
|
||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all h-32 resize-none"
|
||||
></motion.textarea>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<motion.button
|
||||
type="submit"
|
||||
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff", border: "none" }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium transition-all duration-300 w-full md:w-auto shadow-lg"
|
||||
>
|
||||
Send Message
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.form>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const Features: React.FC = () => {
|
||||
const features = [
|
||||
{
|
||||
icon: 'verified_user',
|
||||
title: 'Certified Experts',
|
||||
desc: 'Our team holds top-tier certifications from Microsoft, Cisco, and AWS, ensuring you get world-class expertise.',
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
icon: 'rocket_launch',
|
||||
title: 'Rapid Response',
|
||||
desc: 'Time is money. We guarantee a 15-minute initial response time for critical issues to keep you moving.',
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
icon: 'handshake',
|
||||
title: 'Dedicated Partnership',
|
||||
desc: "We don't just fix computers; we align IT strategy with your business goals for long-term success.",
|
||||
color: 'emerald'
|
||||
}
|
||||
];
|
||||
|
||||
const getColorClasses = (color: string) => {
|
||||
switch(color) {
|
||||
case 'blue': return 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400';
|
||||
case 'purple': return 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400';
|
||||
case 'emerald': return 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400';
|
||||
default: return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
id="features"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="mb-16 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Why Choose Us</span>
|
||||
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
||||
Built on trust. <span className="text-gray-400 dark:text-gray-600">Driven by excellence.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{features.map((feature, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
transition={{ duration: 0.5, delay: i * 0.1 }}
|
||||
whileHover={{ y: -10, boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)" }}
|
||||
className="p-8 border border-gray-200 dark:border-white/10 rounded-xl bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 hover:border-blue-400 dark:hover:border-blue-400 transition-colors duration-300 group"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360, scale: 1.1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center mb-6 ${getColorClasses(feature.color)}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-2xl">{feature.icon}</span>
|
||||
</motion.div>
|
||||
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-3 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{feature.title}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{feature.desc}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Features;
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10 pt-16 pb-8">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<span className="material-symbols-outlined text-xl text-gray-900 dark:text-white">dns</span>
|
||||
<span className="font-display font-bold text-lg tracking-tight text-gray-900 dark:text-white">Bay Area Affiliates</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs mb-6">
|
||||
Track every move, analyze your performance, and get real-time coaching. Your dedicated IT partner in the Bay Area.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{['X', 'in', 'fb'].map((social) => (
|
||||
<motion.a
|
||||
key={social}
|
||||
href="#"
|
||||
whileHover={{ y: -5, borderColor: "#3b82f6", color: "#3b82f6" }}
|
||||
className="w-8 h-8 flex items-center justify-center rounded border border-gray-300 dark:border-white/20 text-gray-600 dark:text-gray-400 transition-colors"
|
||||
>
|
||||
<span className="text-xs font-bold">{social}</span>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Navigation</h4>
|
||||
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{['About', 'Features', 'Testimonials', 'Pricing'].map((item) => (
|
||||
<li key={item}>
|
||||
<motion.a
|
||||
href="#"
|
||||
whileHover={{ x: 5, color: "#3b82f6" }}
|
||||
className="inline-block transition-colors"
|
||||
>
|
||||
{item}
|
||||
</motion.a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Contact</h4>
|
||||
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li>support@bayareaaffiliates.com</li>
|
||||
<li>(361) 765-8400</li>
|
||||
<li>123 Market St, San Francisco, CA</li>
|
||||
<li><motion.a whileHover={{ x: 5, color: "#3b82f6" }} href="#" className="inline-block transition-colors">FAQ</motion.a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-600">
|
||||
© 2024 Bay Area Affiliates, Inc. All rights reserved.
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<motion.a whileHover={{ color: "#3b82f6" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Privacy Policy</motion.a>
|
||||
<motion.a whileHover={{ color: "#3b82f6" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Terms of Service</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useRef, useLayoutEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
// Parallax Background
|
||||
gsap.to(imageRef.current, {
|
||||
yPercent: 30,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: containerRef.current,
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
|
||||
// Text Stagger Animation
|
||||
gsap.fromTo(".hero-stagger",
|
||||
{ y: 50, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
|
||||
);
|
||||
}, containerRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={containerRef} className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
ref={imageRef}
|
||||
alt="Abstract dark technology background"
|
||||
className="w-full h-[120%] -top-[10%] absolute object-cover opacity-90 dark:opacity-60 brightness-50 contrast-125"
|
||||
src="https://lh3.googleusercontent.com/aida-public/AB6AXuDTVpQgmoDeaMvQ7ZXqAWwA3243QWiW39J5KBEt-0pHjhbb4q9ICJb9f42oiDD5becHfElI6zFDQXMpaaMm1Ce5aj4IBVAiIz_XfRzdW-ktaVtom2rkjI9e6AGk71D4YpnXcRtifs5DrbmZfkbp7oBt67fLTwtXLZ_qAic5HprmFz1Xnf2E6emY_trL9Hr4SBtAQL5BE8LHCbF0PrBTK960_Tqz2VhSt_CZfI8UmzBUunqFf54LiDvUsxguYJitq5-r0vWHMVt7xTh1"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background-light via-transparent to-transparent dark:from-background-dark dark:via-transparent dark:to-transparent"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-center max-w-4xl px-6">
|
||||
<div className="hero-stagger flex items-center justify-center gap-2 mb-6">
|
||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">Established 1998</span>
|
||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
||||
</div>
|
||||
|
||||
<h1 className="hero-stagger font-display text-5xl md:text-7xl lg:text-8xl font-medium tracking-tighter leading-[1.1] mb-8 text-gray-900 dark:text-white">
|
||||
Reliable IT Services<br/>
|
||||
<span className="text-gray-500 dark:text-gray-500">for Over 25 Years.</span>
|
||||
</h1>
|
||||
|
||||
<p className="hero-stagger text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-10 font-light leading-relaxed">
|
||||
Bay Area Affiliates is your silent partner in technology. We provide the infrastructure that whispers clarity, ensures uptime, and guides your business growth.
|
||||
</p>
|
||||
|
||||
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<motion.a
|
||||
href="#services"
|
||||
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium"
|
||||
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
Explore Services
|
||||
</motion.a>
|
||||
<motion.a
|
||||
href="#contact"
|
||||
className="px-8 py-3 bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-full font-medium"
|
||||
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.1)", borderColor: "#ffffff" }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
Get a Consultation
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const Mission: React.FC = () => {
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="py-24 bg-background-light dark:bg-background-dark relative"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-start">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4 text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
||||
Our Mission
|
||||
</div>
|
||||
<h2 className="font-display text-4xl md:text-5xl font-medium mb-6 leading-tight text-gray-900 dark:text-white">
|
||||
Harness invisible power <span className="text-gray-400 dark:text-gray-600">to operate faster, focus deeper, and scale effortlessly.</span>
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
className="pt-4"
|
||||
>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 leading-relaxed mb-8">
|
||||
Technology shouldn't be a hurdle; it should be the wind at your back. From seamless cloud migrations to robust cybersecurity, we handle the complexities so you can focus on what matters most: your business.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-8 border-t border-gray-200 dark:border-white/10 pt-8">
|
||||
<motion.div whileHover={{ scale: 1.05 }} className="cursor-default">
|
||||
<span className="block text-3xl font-display font-bold mb-2 text-gray-900 dark:text-white">99.9%</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-500">Uptime Guarantee</span>
|
||||
</motion.div>
|
||||
<motion.div whileHover={{ scale: 1.05 }} className="cursor-default">
|
||||
<span className="block text-3xl font-display font-bold mb-2 text-gray-900 dark:text-white">24/7</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-500">Support Availability</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mission;
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useState } from 'react';
|
||||
import { motion, useScroll, useMotionValueEvent, useSpring } from 'framer-motion';
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const { scrollY, scrollYProgress } = useScroll();
|
||||
|
||||
const scaleX = useSpring(scrollYProgress, {
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
restDelta: 0.001
|
||||
});
|
||||
|
||||
useMotionValueEvent(scrollY, "change", (latest) => {
|
||||
const previous = scrollY.getPrevious() || 0;
|
||||
if (latest > previous && latest > 150) {
|
||||
setHidden(true);
|
||||
} else {
|
||||
setHidden(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.nav
|
||||
variants={{
|
||||
visible: { y: 0 },
|
||||
hidden: { y: "-100%" },
|
||||
}}
|
||||
animate={hidden ? "hidden" : "visible"}
|
||||
transition={{ duration: 0.35, ease: "easeInOut" }}
|
||||
className="fixed w-full z-40 top-0 left-0 border-b border-gray-200 dark:border-white/10 bg-white/80 dark:bg-background-dark/80 backdrop-blur-md"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 180 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl dark:text-white text-black">dns</span>
|
||||
</motion.div>
|
||||
<span className="font-display font-bold text-lg tracking-tight">Bay Area Affiliates</span>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{['Services', 'Features', 'Blog', 'Contact'].map((item) => (
|
||||
<motion.a
|
||||
key={item}
|
||||
href={`#${item.toLowerCase()}`}
|
||||
className="hover:text-black dark:hover:text-white transition-colors relative group px-2 py-1"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{item}
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-black dark:bg-white transition-all duration-300 ease-out group-hover:w-full"></span>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.a
|
||||
href="#"
|
||||
className="text-sm font-medium bg-black dark:bg-white text-white dark:text-black px-4 py-2 rounded-full transition-all"
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
boxShadow: "0px 0px 15px rgba(100, 100, 100, 0.5)"
|
||||
}}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
Client Portal
|
||||
</motion.a>
|
||||
</div>
|
||||
|
||||
{/* Scroll Progress Indicator */}
|
||||
<motion.div
|
||||
className="absolute bottom-0 left-0 right-0 h-[2px] bg-blue-600 dark:bg-blue-500 origin-left z-50"
|
||||
style={{ scaleX }}
|
||||
/>
|
||||
</motion.nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import React, { useLayoutEffect, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const Process: React.FC = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imageColRef = useRef<HTMLDivElement>(null);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const textColRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
// Fade in the whole section
|
||||
gsap.fromTo(containerRef.current,
|
||||
{ opacity: 0, y: 50 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: containerRef.current,
|
||||
start: "top 80%",
|
||||
once: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Desktop specific animations
|
||||
const mm = gsap.matchMedia();
|
||||
|
||||
mm.add("(min-width: 1024px)", () => {
|
||||
if (containerRef.current && imageColRef.current && imgRef.current) {
|
||||
// Pinning logic
|
||||
ScrollTrigger.create({
|
||||
trigger: containerRef.current,
|
||||
start: "top center",
|
||||
end: "bottom center",
|
||||
pin: imageColRef.current,
|
||||
pinSpacing: false,
|
||||
scrub: true,
|
||||
});
|
||||
|
||||
// Scroll-to-Zoom logic
|
||||
gsap.fromTo(imgRef.current,
|
||||
{ scale: 1 },
|
||||
{
|
||||
scale: 2.2,
|
||||
ease: "power1.inOut",
|
||||
scrollTrigger: {
|
||||
trigger: containerRef.current,
|
||||
start: "top bottom",
|
||||
end: "bottom top",
|
||||
scrub: 1,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Animate steps as they come into view
|
||||
const steps = gsap.utils.toArray('.process-step');
|
||||
steps.forEach((step: any) => {
|
||||
gsap.fromTo(step,
|
||||
{ opacity: 0.3, x: 20 },
|
||||
{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
duration: 0.5,
|
||||
scrollTrigger: {
|
||||
trigger: step,
|
||||
start: "top 80%",
|
||||
end: "top 50%",
|
||||
scrub: 0.5,
|
||||
toggleActions: "play reverse play reverse"
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}, containerRef);
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={containerRef} className="py-24 bg-background-light dark:bg-background-dark overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start relative">
|
||||
|
||||
{/* Image Column - Will be pinned */}
|
||||
<div ref={imageColRef} className="relative rounded-2xl overflow-hidden border border-gray-200 dark:border-white/10 shadow-2xl h-[400px] lg:h-[400px] w-full max-w-lg lg:max-w-none mx-auto group z-10">
|
||||
<img
|
||||
ref={imgRef}
|
||||
alt="Close up of server lights in dark room"
|
||||
className="w-full h-full object-cover opacity-90 will-change-transform origin-center scale-100"
|
||||
src="https://lh3.googleusercontent.com/aida-public/AB6AXuD6sSjDiHv-HJ5pcRY_PSPYLWxc5ePEHS5R5qi86rya93pqyTkDAG2t__6s26GhoK1LKnSPbBcqi2cfiVn8A02_G47nrQU8REa-C-4oC8wITbxnE79seVsSNsg-6pA58AgqgT4WBextocvu9UYIxQo0Hkymge7c870jS6yGtkUxgJR6RH6_HKnKs03tA7yYClVL2l6ObnPSvfXprP2XRs_GJHWm0bDD_0LhX0eCnXSkUdrYV4_LBvAFosluY1t6lmI2NUcO0diZffZZ"
|
||||
/>
|
||||
<div className="absolute bottom-6 left-6 right-6 p-6 bg-white/5 backdrop-blur-md rounded-lg border border-white/10 pointer-events-none">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="material-symbols-outlined text-white text-sm">construction</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-medium mb-1">On-Site Support</h4>
|
||||
<p className="text-gray-400 text-xs leading-relaxed">Technicians dispatched within 2 hours for critical failures in the Bay Area.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<div ref={textColRef} className="lg:py-12">
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Process</span>
|
||||
<h2 className="font-display text-4xl font-medium mb-6 text-gray-900 dark:text-white">
|
||||
One consultation to begin,<br/>
|
||||
<span className="text-gray-400 dark:text-gray-600">three steps to clarity.</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-16 mt-16">
|
||||
{[
|
||||
{ num: "1", title: "Audit & Assess", desc: "We dive deep into your current infrastructure to identify vulnerabilities and opportunities for optimization." },
|
||||
{ num: "2", title: "Implement & Secure", desc: "Our team deploys the necessary hardware and software solutions with minimal disruption to your daily operations." },
|
||||
{ num: "3", title: "Monitor & Maintain", desc: "Ongoing 24/7 monitoring ensures problems are solved before you even notice them." }
|
||||
].map((step, i) => (
|
||||
<div key={i} className="process-step flex gap-6 group cursor-default">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<motion.span
|
||||
whileHover={{ scale: 1.2, borderColor: "#3b82f6", color: "#3b82f6" }}
|
||||
className="flex items-center justify-center w-8 h-8 rounded border border-gray-300 dark:border-white/20 text-sm font-medium text-gray-500 dark:text-gray-400 transition-colors"
|
||||
>
|
||||
{step.num}
|
||||
</motion.span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium text-gray-900 dark:text-white group-hover:translate-x-1 transition-transform group-hover:text-blue-500">{step.title}</h3>
|
||||
<p className="text-base text-gray-600 dark:text-gray-400 mt-2 leading-relaxed">
|
||||
{step.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Process;
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const servicesData = [
|
||||
{
|
||||
id: 1,
|
||||
category: 'IT Infrastructure',
|
||||
title: 'Windows 11 Transition',
|
||||
description: 'Seamless upgrades for your entire fleet. We handle compatibility checks, data backup, and deployment so your workflow never stutters.',
|
||||
icon: 'desktop_windows',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: 'Security',
|
||||
title: 'Web Services & Security',
|
||||
description: 'From hosting to rigorous penetration testing. Secure your digital storefront with enterprise-grade protection and 99.9% uptime.',
|
||||
icon: 'security',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: 'Consulting',
|
||||
title: 'Performance Upgrades',
|
||||
description: 'Is your hardware holding you back? We analyze bottlenecks and implement strategic upgrades to memory, storage, and networks.',
|
||||
icon: 'speed',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
|
||||
}
|
||||
];
|
||||
|
||||
const categories = ['All', 'IT Infrastructure', 'Web Development', 'Consulting', 'Security'];
|
||||
|
||||
const Services: React.FC = () => {
|
||||
const [activeCategory, setActiveCategory] = useState('All');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
// Reset refs on render to handle filtering updates
|
||||
imagesRef.current = [];
|
||||
|
||||
const filteredServices = activeCategory === 'All'
|
||||
? servicesData
|
||||
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
imagesRef.current.forEach((imgWrapper) => {
|
||||
if (!imgWrapper) return;
|
||||
|
||||
gsap.to(imgWrapper, {
|
||||
yPercent: 30,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: imgWrapper.closest('.group'),
|
||||
start: "top bottom",
|
||||
end: "bottom top",
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}, containerRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [filteredServices]);
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
ref={containerRef}
|
||||
id="services"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="mb-16">
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Core Offerings</span>
|
||||
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
||||
Different paths to explore <span className="text-gray-400 dark:text-gray-600">all guided by one expert team.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`pb-2 whitespace-nowrap transition-colors relative ${
|
||||
activeCategory === cat
|
||||
? 'text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
{activeCategory === cat && (
|
||||
<motion.div
|
||||
layoutId="activeTab"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredServices.map((service) => (
|
||||
<motion.div
|
||||
key={service.id}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
whileHover={{ y: -10, transition: { duration: 0.3 } }}
|
||||
className="group relative bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden hover:border-blue-500/50 dark:hover:border-blue-500/50 hover:shadow-2xl transition-all duration-300"
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="h-48 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
||||
{/* Parallax Wrapper */}
|
||||
<div
|
||||
ref={el => { if (el) imagesRef.current.push(el); }}
|
||||
className="w-full h-[140%] -mt-[20%]"
|
||||
>
|
||||
<img
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-80 group-hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 relative">
|
||||
<motion.div
|
||||
className="w-10 h-10 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-4 border border-gray-200 dark:border-white/10"
|
||||
whileHover={{ rotate: 360, backgroundColor: "#3b82f6", color: "white", borderColor: "#3b82f6" }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span>
|
||||
</motion.div>
|
||||
<h3 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-4">
|
||||
{service.description}
|
||||
</p>
|
||||
<a href="#" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-blue-500 transition-colors">
|
||||
Learn More <motion.span
|
||||
className="material-symbols-outlined text-xs ml-1"
|
||||
animate={{ x: [0, 5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", repeatDelay: 1 }}
|
||||
>arrow_forward</motion.span>
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Services;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bay Area Affiliates, Inc. - Modern IT Solutions</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#FFFFFF",
|
||||
"background-light": "#F3F4F6",
|
||||
"background-dark": "#0a0a0a",
|
||||
"surface-dark": "#121212",
|
||||
"text-muted": "#888888",
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['"Space Grotesk"', 'sans-serif'],
|
||||
sans: ['"Inter"', 'sans-serif'],
|
||||
},
|
||||
borderRadius: {
|
||||
DEFAULT: "4px",
|
||||
'lg': "8px",
|
||||
'xl': "12px",
|
||||
},
|
||||
backgroundImage: {
|
||||
'grain': "url('data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.05%22/%3E%3C/svg%3E')",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
html {
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
body {
|
||||
background-color: #0a0a0a;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.lenis.lenis-smooth {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.lenis.lenis-stopped {
|
||||
overflow: hidden;
|
||||
}
|
||||
.lenis.lenis-scrolling iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"react/": "https://esm.sh/react@^19.2.3/",
|
||||
"framer-motion": "https://esm.sh/framer-motion@^12.26.2",
|
||||
"gsap": "https://esm.sh/gsap@^3.14.2",
|
||||
"gsap/": "https://esm.sh/gsap@^3.14.2/",
|
||||
"@studio-freight/lenis": "https://esm.sh/@studio-freight/lenis@^1.0.42"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background-light dark:bg-background-dark text-gray-900 dark:text-white font-sans antialiased selection:bg-white selection:text-black transition-colors duration-300">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Bay Area Affiliates",
|
||||
"description": "Modern IT Solutions corporate website featuring smooth GSAP scrolling, Framer Motion animations, and a premium dark-mode aesthetic.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "bay-area-affiliates",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"framer-motion": "^12.26.2",
|
||||
"gsap": "^3.14.2",
|
||||
"@studio-freight/lenis": "^1.0.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue