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:
knuthtimo-lab 2026-01-15 18:27:22 +01:00
parent 4e3516d96a
commit 1a85f0eb2d
19 changed files with 1260 additions and 8 deletions

24
.gitignore vendored Normal file
View File

@ -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?

74
App.tsx Normal file
View File

@ -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>
);
}

View File

@ -1,11 +1,20 @@
<div align="center"> <div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" /> <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> </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`

45
components/BackToTop.tsx Normal file
View File

@ -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;

131
components/Blog.tsx Normal file
View File

@ -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;

86
components/Contact.tsx Normal file
View File

@ -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;

82
components/Features.tsx Normal file
View File

@ -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;

73
components/Footer.tsx Normal file
View File

@ -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;

88
components/Hero.tsx Normal file
View File

@ -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;

57
components/Mission.tsx Normal file
View File

@ -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;

81
components/Navbar.tsx Normal file
View File

@ -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;

153
components/Process.tsx Normal file
View File

@ -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;

167
components/Services.tsx Normal file
View File

@ -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;

86
index.html Normal file
View File

@ -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>

15
index.tsx Normal file
View File

@ -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>
);

5
metadata.json Normal file
View File

@ -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": []
}

24
package.json Normal file
View File

@ -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"
}
}

29
tsconfig.json Normal file
View File

@ -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
}
}

23
vite.config.ts Normal file
View File

@ -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, '.'),
}
}
};
});