Neue services

This commit is contained in:
2026-03-25 20:07:27 -05:00
parent 42e0971a13
commit bcf9dc541c
85 changed files with 8589 additions and 4832 deletions

View File

@@ -3,6 +3,10 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { locationData } from '../src/data/seoData';
const cityLocations = locationData.filter(loc =>
loc.slug.startsWith('locations/it-support-')
);
const AreasWeServe: React.FC = () => {
return (
<section className="py-24 px-6 bg-gray-50 dark:bg-white/5 mx-auto text-center border-t border-gray-200 dark:border-white/10">
@@ -12,11 +16,11 @@ const AreasWeServe: React.FC = () => {
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed">
We provide professional IT support and IT services for businesses throughout Corpus Christi and the surrounding Coastal Bend area.
Our team supports local companies with business IT support, outsourced IT services, and help desk solutions, delivered remotely or on-site when needed.
Our team supports local companies with outsourced IT services and help desk solutions, delivered remotely or on-site when needed.
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-12">
{locationData.map((loc) => (
{cityLocations.map((loc) => (
<Link
key={loc.slug}
to={`/${loc.slug}`}
@@ -31,8 +35,8 @@ const AreasWeServe: React.FC = () => {
Not sure if your location is covered? <Link to="/contact" className="underline hover:text-black dark:hover:text-white transition-colors">Contact us today</Link> to discuss your IT needs.
</p>
<div className="mt-6">
<Link to="/it-support-corpus-christi" className="text-sm text-gray-500 hover:text-gray-800 dark:hover:text-gray-300 transition-colors">
Get local IT support in Corpus Christi and nearby areas
<Link to="/locations" className="text-sm text-gray-500 hover:text-gray-800 dark:hover:text-gray-300 transition-colors">
View all service areas
</Link>
</div>
</div>

View File

@@ -1,45 +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>
);
};
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;

View File

@@ -2,97 +2,86 @@ 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: 'Jan 10, 2026',
category: 'Performance',
title: 'Upgrade Your HDD to SSD for Enhanced Performance',
excerpt: 'In today\'s fast-paced digital world, the performance of your computer can make a significant difference in productivity...'
},
{
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCz5lTYjY4RNXubQlrA-BtLIGR3nUY8ULkD9omwT5FShfdMrbMgS5dDCyfN3xiB5WC7T3vjNvyvVbvnD0G1zBpbNTjfOYyhmAEfno7Hf5W1sm-KYRXYrLGQq-c6TkLgEf0i9JGNvuFZ6edcenr2o39dCzIPXcp_z9XWOIzp7kBX2EydNPLJoRofVYuSTmEA1y0_xh4sdiRy1PykRASGLhKfN19_XLNuwyTBVKYISY7cHc-An69eZpAfhrvngu3E47rU6KuQS0k3QXBZ',
date: 'Jan 5, 2026',
category: 'Security',
title: 'Secure Your Corporate Network Access with WireGuard VPN',
excerpt: 'The safest way to access your corporate network remotely is through a secure VPN connection...'
},
{
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCl5iOhTsCqcHnho89DkoLh0DYeuvef0pdp8k26NKzcAq7YPvWbAYARg9mCIvqGTxQGradp8zvscuuibskpz4W_nEzQQO1z7lgwKJ1Xxiw_yQOyXMLfoRNLTHXzqFUH8Q5daCAfYTb7Zl3sFjB7k8i44D6TGolzqrN05Db27Abf2TWDDzHpVSrNml4zddvxholHFxMzqDeSzQ5p77SLDSFNaYBZGR2lEdN2V9O0GzMqxbOjFmBGMW48nlrEDLDzYGv_gWI3RSqNqBl-',
date: 'Dec 28, 2025',
category: 'Infrastructure',
title: 'Virtualizing Windows Machines: Future-Proof Your Corporate Network',
excerpt: 'In October 2025, Microsoft will end support for Windows 10. Learn how virtualization can help you prepare...'
}
];
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
>
<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">
Stay updated <span className="text-gray-400 dark:text-gray-600">with our latest news and articles.</span>
</h2>
</div>
<motion.a
href="#"
import { Link } from 'react-router-dom';
import { blogPostData } from '../src/data/seoData';
gsap.registerPlugin(ScrollTrigger);
const posts = blogPostData
.filter((post) => !post.redirect)
.slice(0, 3)
.map((post) => ({
image: post.image || '/images/blog/business-email-comparison-new.png',
category: post.category === 'authority' ? 'IT Insights' : 'Local Services',
title: post.h1,
excerpt: post.description,
href: `/${post.slug}`,
}));
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
>
<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">
Stay updated <span className="text-gray-400 dark:text-gray-600">with our latest news and articles.</span>
</h2>
</div>
<Link
to="/blog"
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"
</Link>
</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"
>
<Link to={post.href} className="block">
<div className="h-64 rounded-xl overflow-hidden mb-6 relative shadow-lg">
<div
ref={el => { if (el) imagesRef.current.push(el); }}
@@ -101,17 +90,18 @@ const Blog: React.FC = () => {
<img
src={post.image}
alt={post.title}
loading="lazy"
decoding="async"
fetchPriority="low"
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 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">
@@ -120,12 +110,13 @@ const Blog: React.FC = () => {
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{post.excerpt}
</p>
</Link>
</motion.article>
))}
</div>
</div>
</motion.section>
);
};
export default Blog;
</div>
</motion.section>
);
};
export default Blog;

35
components/Breadcrumb.tsx Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Link } from 'react-router-dom';
interface BreadcrumbItem {
label: string;
to?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
const Breadcrumb: React.FC<BreadcrumbProps> = ({ items }) => {
return (
<nav aria-label="breadcrumb" className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
{items.map((item, i) => (
<React.Fragment key={i}>
{i > 0 && <span className="text-gray-400 dark:text-gray-600">/</span>}
{item.to ? (
<Link
to={item.to}
className="hover:text-gray-900 dark:hover:text-white transition-colors"
>
{item.label}
</Link>
) : (
<span className="text-gray-900 dark:text-white font-medium">{item.label}</span>
)}
</React.Fragment>
))}
</nav>
);
};
export default Breadcrumb;

View File

@@ -21,7 +21,7 @@ const CTA: React.FC = () => {
transition={{ delay: 0.1 }}
className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed"
>
Join 150+ Coastal Bend businesses that trust us with their technology. Get started with a free 20-minute assessment.
Join 30+ Coastal Bend businesses that trust us with their technology. Get started with a free 20-minute assessment.
</motion.p>
<motion.div
@@ -43,6 +43,12 @@ const CTA: React.FC = () => {
>
Send a message
</Link>
<a
href="tel:+13617658400"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors mt-2"
>
Or call us: (361) 765-8400
</a>
</motion.div>
<motion.div

View File

@@ -1,113 +1,113 @@
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">
We're here to help you with all your IT needs.
</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="Your Name"
required
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="Your Email"
required
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 className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone (optional)</label>
<motion.input
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
transition={{ duration: 0.2 }}
type="tel"
id="phone"
placeholder="Your Phone Number"
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="company" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Company (optional)</label>
<motion.input
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
transition={{ duration: 0.2 }}
type="text"
id="company"
placeholder="Your Company Name"
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="Your Message"
required
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>
);
};
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">
We're here to help you with all your IT needs.
</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="Your Name"
required
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="Your Email"
required
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 className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone (optional)</label>
<motion.input
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
transition={{ duration: 0.2 }}
type="tel"
id="phone"
placeholder="Your Phone Number"
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="company" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Company (optional)</label>
<motion.input
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
transition={{ duration: 0.2 }}
type="text"
id="company"
placeholder="Your Company Name"
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="Your Message"
required
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;

View File

@@ -1,68 +1,109 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
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="grid grid-cols-1 md:grid-cols-5 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>
<span className="font-display font-bold text-lg tracking-tight text-gray-900 dark:text-white">Bay Area IT</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs mb-6">
Providing reliable IT services and solutions to the Coastal Bend community for over 25 years.
Providing reliable IT services and practical technology support to the Coastal Bend community for over 25 years.
</p>
<div className="flex gap-4">
{['X', 'in', 'fb'].map((social) => (
<motion.a
key={social}
href="#"
whileHover={{ y: -5, borderColor: "#ffffff", color: "#ffffff" }}
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 className="flex flex-wrap gap-3 text-sm text-gray-600 dark:text-gray-400">
<a href="tel:+13617658400" className="transition-colors hover:text-gray-900 dark:hover:text-white">
(361) 765-8400
</a>
<a href="mailto:info@bayareaaffiliates.com" className="transition-colors hover:text-gray-900 dark:hover:text-white">
info@bayareaaffiliates.com
</a>
</div>
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Navigation</h4>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Services</h4>
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
{['Services', 'Features', 'Blog', 'Contact'].map((item) => (
<li key={item}>
<motion.a
href="#"
whileHover={{ x: 5, color: "#ffffff" }}
className="inline-block transition-colors"
>
{item}
</motion.a>
{[
{ label: 'IT Help Desk', to: '/services/it-help-desk' },
{ label: 'Computer Support', to: '/services/computer-support' },
{ label: 'Business Email', to: '/services/business-email-services' },
{ label: 'Domain & DNS', to: '/services/domain-registration-dns-support' },
{ label: 'Web Design', to: '/services/web-design-corpus-christi' },
].map((item) => (
<li key={item.label}>
<motion.div whileHover={{ x: 5 }} className="inline-block">
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
{item.label}
</Link>
</motion.div>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Contact</h4>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Areas We Serve</h4>
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
<li>support@bayareaaffiliates.com</li>
{[
{ label: 'Corpus Christi', to: '/locations/it-support-corpus-christi' },
{ label: 'Portland, TX', to: '/locations/it-support-portland-tx' },
{ label: 'Rockport, TX', to: '/locations/it-support-rockport-tx' },
{ label: 'Aransas Pass', to: '/locations/it-support-aransas-pass-tx' },
{ label: 'Kingsville', to: '/locations/it-support-kingsville-tx' },
].map((item) => (
<li key={item.label}>
<motion.div whileHover={{ x: 5 }} className="inline-block">
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
{item.label}
</Link>
</motion.div>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Company</h4>
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
{[
{ label: 'About', to: '/about' },
{ label: 'Blog', to: '/blog' },
{ label: 'Contact', to: '/contact' },
{ label: 'Privacy Policy', to: '/privacy-policy' },
{ label: 'Terms of Service', to: '/terms-of-service' },
].map((item) => (
<li key={item.label}>
<motion.div whileHover={{ x: 5 }} className="inline-block">
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
{item.label}
</Link>
</motion.div>
</li>
))}
<li>
<a href="mailto:info@bayareaaffiliates.com" className="hover:text-gray-900 dark:hover:text-white transition-colors">info@bayareaaffiliates.com</a>
</li>
<li>(361) 765-8400</li>
<li>1001 Blucher St, Corpus Christi, TX 78401</li>
<li><motion.a whileHover={{ x: 5, color: "#ffffff" }} 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">
© 2026 Bay Area Affiliates, Inc. All rights reserved.
© 2026 Bay Area IT. All rights reserved.
</p>
<div className="flex gap-6">
<motion.a whileHover={{ color: "#ffffff" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Privacy Policy</motion.a>
<motion.a whileHover={{ color: "#ffffff" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Terms of Service</motion.a>
<div className="flex flex-wrap items-center justify-center gap-4 text-xs text-gray-500 dark:text-gray-600">
<Link to="/privacy-policy" className="transition-colors hover:text-gray-900 dark:hover:text-white">
Privacy Policy
</Link>
<Link to="/terms-of-service" className="transition-colors hover:text-gray-900 dark:hover:text-white">
Terms of Service
</Link>
</div>
</div>
</div>
@@ -70,4 +111,4 @@ const Footer: React.FC = () => {
);
};
export default Footer;
export default Footer;

View File

@@ -1,115 +1,149 @@
import React, { useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
import heroBg from '../src/assets/hero-bg.webp';
gsap.registerPlugin(ScrollTrigger);
const Hero: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const prefersReducedMotion = useReducedMotion();
const [isInteractive, setIsInteractive] = useState(false);
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
useEffect(() => {
if (prefersReducedMotion || typeof window === 'undefined') {
setIsInteractive(false);
return;
}
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
const updateState = () => setIsInteractive(mediaQuery.matches);
updateState();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateState);
return () => mediaQuery.removeEventListener('change', updateState);
}
mediaQuery.addListener(updateState);
return () => mediaQuery.removeListener(updateState);
}, [prefersReducedMotion]);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
if (!isInteractive) return;
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
if (!isInteractive) {
return;
}
const ctx = gsap.context(() => {
// Parallax Background
gsap.to(parallaxWrapperRef.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();
}, []);
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();
}, [isInteractive]);
return (
<section
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseMove={isInteractive ? handleMouseMove : undefined}
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
>
<div className="absolute inset-0 z-0 pointer-events-none">
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
{/* Base Layer - Slightly Brighter */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
{/* Base Layer - Slightly Brighter */}
<img
alt="Abstract dark technology background"
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
src="/src/assets/hero-bg.png"
src={heroBg}
loading="eager"
decoding="async"
fetchPriority="high"
/>
{/* Highlight Layer - Only visible via mask */}
<motion.img
style={{
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
}}
alt=""
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src="/src/assets/hero-bg.png"
/>
{isInteractive && (
<motion.img
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src={heroBg}
loading="lazy"
decoding="async"
/>
)}
</div>
<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">
Providing top-notch Computer & Networking solutions to the Coastal Bend community.
<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">Serving the Coastal Bend since 2000</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">
Local IT support, help desk coverage, networking, email, and practical technology support for Coastal Bend businesses.
</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 }}
>
IT 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 in Touch
</motion.a>
</div>
</div>
</section>
);
};
export default Hero;
<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 }}
>
IT 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 in Touch
</motion.a>
</div>
</div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,111 @@
import React, { useEffect, useRef } from 'react';
import gsap from 'gsap';
interface LoadingScreenProps {
onComplete: () => void;
}
const LoadingScreen: React.FC<LoadingScreenProps> = ({ onComplete }) => {
const containerRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const progressRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Hide scrolling on the body while loading
document.body.style.overflow = 'hidden';
const tl = gsap.timeline({
onComplete: () => {
document.body.style.overflow = '';
onComplete();
},
});
const chars = textRef.current?.querySelectorAll('.char');
// 1. Initial state: progress bar width 0
gsap.set(progressRef.current, { scaleX: 0, transformOrigin: 'left center' });
if (chars) {
gsap.set(chars, { yPercent: 100, opacity: 0 });
}
// 2. Animate the progress bar and reveal text simultaneously
tl.to(progressRef.current, {
scaleX: 1,
duration: 1.5,
ease: 'power3.inOut',
})
.to(
chars || [],
{
yPercent: 0,
opacity: 1,
duration: 0.8,
stagger: 0.05,
ease: 'power4.out',
},
'<0.5' // Start this animation 0.5s into the progress bar loading
)
// 3. Keep it visible briefly
.to({}, { duration: 0.3 })
// 4. Slide out the individual characters upwards
.to(
chars || [],
{
yPercent: -100,
opacity: 0,
duration: 0.5,
stagger: 0.02,
ease: 'power3.in',
}
)
// 5. Fade out progress line
.to(progressRef.current, { opacity: 0, duration: 0.3 }, '<')
// 6. Slide the whole curtain up to reveal the homepage
.to(containerRef.current, {
yPercent: -100,
duration: 1,
ease: 'expo.inOut',
});
return () => {
tl.kill();
document.body.style.overflow = '';
};
}, [onComplete]);
const text = 'BAY AREA IT';
return (
<div
ref={containerRef}
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-surface-dark dark:bg-background-dark text-primary"
>
<div className="relative overflow-hidden mb-8">
<h1
ref={textRef}
className="font-display text-4xl md:text-6xl font-bold tracking-widest flex overflow-hidden"
>
{text.split('').map((char, index) => (
<span
key={index}
className={`char inline-block opacity-0 ${char === ' ' ? 'w-4' : ''}`}
>
{char}
</span>
))}
</h1>
</div>
{/* Sleek Minimalist Loading Line */}
<div className="w-48 md:w-64 h-[2px] bg-white/10 rounded-full overflow-hidden">
<div
ref={progressRef}
className="h-full w-full bg-primary origin-left scale-x-0"
></div>
</div>
</div>
);
};
export default LoadingScreen;

View File

@@ -1,63 +1,63 @@
import React from 'react';
import { motion } from 'framer-motion';
import Counter from './Counter';
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
>
<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: -200 }}
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-gray-400 dark:bg-gray-600"></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: 200 }}
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 flex items-center">
<Counter value={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 flex items-center">
<Counter value={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>
);
};
import React from 'react';
import { motion } from 'framer-motion';
import Counter from './Counter';
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
>
<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: -200 }}
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-gray-400 dark:bg-gray-600"></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: 200 }}
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 flex items-center">
<Counter value={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 flex items-center">
<Counter value={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;

View File

@@ -1,49 +1,48 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion } from 'framer-motion';
const Navbar: React.FC = () => {
const location = useLocation();
const isHome = location.pathname === '/';
return (
<nav
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">
<Link to="/" 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>
</Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-gray-600 dark:text-gray-400">
{['About', 'Services', 'Blog', 'Contact'].map((item) => (
<Link
key={item}
to={`/${item.toLowerCase()}`}
className="hover:text-black dark:hover:text-white transition-colors relative group px-2 py-1"
>
<motion.span
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-block"
>
{item}
</motion.span>
<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>
</Link>
))}
</div>
{/* Client Portal button removed */}
</div>
</nav>
);
};
export default Navbar;
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
const Navbar: React.FC = () => {
const location = useLocation();
const isHome = location.pathname === '/';
return (
<nav
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">
<Link to="/" className="flex items-center gap-2">
<img
src="/logo.svg"
alt="Bay Area IT logo"
className="h-8 w-8"
/>
<span className="font-display font-bold text-lg tracking-tight">Bay Area IT</span>
</Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-gray-600 dark:text-gray-400">
{['About', 'Services', 'Blog', 'Contact'].map((item) => (
<Link
key={item}
to={`/${item.toLowerCase()}`}
className="hover:text-black dark:hover:text-white transition-colors relative group px-2 py-1"
>
<span className="inline-block">
{item}
</span>
<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>
</Link>
))}
</div>
<Link
to="/contact"
className="hidden md:inline-flex items-center px-4 py-1.5 bg-black dark:bg-white text-white dark:text-black rounded-full text-sm font-medium hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors"
>
Get IT Support
</Link>
</div>
</nav>
);
};
export default Navbar;

View File

@@ -1,119 +1,149 @@
import React, { useLayoutEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
import processIllustration from '../src/assets/process-illustration.webp';
gsap.registerPlugin(ScrollTrigger);
const Process: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const prefersReducedMotion = useReducedMotion();
const [supportsFinePointer, setSupportsFinePointer] = useState(false);
const shouldAnimate = !prefersReducedMotion && supportsFinePointer;
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
const updateState = () => setSupportsFinePointer(mediaQuery.matches);
updateState();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateState);
return () => mediaQuery.removeEventListener('change', updateState);
}
mediaQuery.addListener(updateState);
return () => mediaQuery.removeListener(updateState);
}, []);
useLayoutEffect(() => {
if (!shouldAnimate) {
return;
}
const ctx = gsap.context((self) => {
// Dramatic Zoom Animation
if (containerRef.current && imgRef.current) {
gsap.fromTo(imgRef.current,
{ scale: 1, transformOrigin: 'center center' },
{
scale: 2.0,
ease: "none",
scrollTrigger: {
trigger: containerRef.current,
start: "top bottom",
end: "bottom top",
scrub: true,
}
}
);
}
// Animate steps - even slower, one by one appearance
const steps = gsap.utils.selector(containerRef.current)('.process-step');
steps.forEach((step: any, index: number) => {
gsap.fromTo(step,
{ opacity: 0, y: 60 },
{
opacity: 1,
y: 0,
duration: 2,
ease: "power3.out",
scrollTrigger: {
trigger: step,
start: "top 95%",
end: "top 40%",
toggleActions: "play reverse play reverse",
scrub: 1.5
}
}
);
});
gsap.fromTo(imgRef.current,
{ scale: 1, transformOrigin: 'center center' },
{
scale: 2.0,
ease: "none",
scrollTrigger: {
trigger: containerRef.current,
start: "top bottom",
end: "bottom top",
scrub: true,
}
}
);
}
// Animate steps - even slower, one by one appearance
const steps = gsap.utils.selector(containerRef.current)('.process-step');
steps.forEach((step: any, index: number) => {
gsap.fromTo(step,
{ opacity: 0, y: 60 },
{
opacity: 1,
y: 0,
duration: 2,
ease: "power3.out",
scrollTrigger: {
trigger: step,
start: "top 95%",
end: "top 40%",
toggleActions: "play reverse play reverse",
scrub: 1.5
}
}
);
});
}, containerRef);
return () => ctx.revert();
}, []);
}, [shouldAnimate]);
return (
<section ref={containerRef} className="relative w-full" style={{ clipPath: 'inset(0)' }}>
{/* Fixed Background Image - constrained to this section via clip-path */}
<div className="fixed inset-0 w-full h-screen z-0 overflow-hidden">
<div className={`${shouldAnimate ? 'fixed inset-0 w-full h-screen' : 'absolute inset-0 w-full h-full'} z-0 overflow-hidden`}>
<img
ref={imgRef}
alt="Modern server rack infrastructure"
className="w-full h-full object-cover opacity-80 will-change-transform origin-center"
src="/src/assets/process-illustration.png"
className={`w-full h-full object-cover opacity-80 origin-center ${shouldAnimate ? 'will-change-transform' : ''}`}
src={processIllustration}
loading="lazy"
decoding="async"
fetchPriority="low"
/>
{/* Gradient overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-r from-black/50 via-black/30 to-black/60" />
</div>
{/* Content - positioned relative, scrolls over the fixed image */}
<div className="relative z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))]">
{/* Header - Static on mobile, fixed on desktop */}
<div className="relative mb-12 lg:mb-0 lg:fixed lg:top-1/2 lg:right-16 lg:-translate-y-1/2 z-20 text-left lg:text-right px-6 lg:px-0" style={{ clipPath: 'none' }}>
<span className="text-xs font-semibold uppercase tracking-widest text-gray-300 mb-2 block">Process</span>
<h2 className="font-display text-3xl lg:text-5xl font-medium text-white">
One consultation to begin,<br />
<span className="text-gray-400">three steps to clarity.</span>
</h2>
</div>
{/* Spacer for first screen - shortened */}
<div className="h-[30vh]" />
{/* Steps - LEFT side on desktop, full width on mobile */}
<div className="min-h-screen px-6 lg:px-16 py-24">
<div className="w-full lg:w-1/2 space-y-[60vh]">
{[
{ 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 bg-black/60 backdrop-blur-md p-8 rounded-2xl border border-white/10">
<div className="flex-shrink-0 mt-1">
<motion.span
whileHover={{ scale: 1.2, borderColor: "#3b82f6", color: "#3b82f6" }}
className="flex items-center justify-center w-12 h-12 rounded-xl border-2 border-white/30 text-lg font-bold text-white transition-colors"
>
{step.num}
</motion.span>
</div>
<div>
<h3 className="text-2xl lg:text-3xl font-medium text-white group-hover:translate-x-1 transition-transform group-hover:text-blue-400">{step.title}</h3>
<p className="text-lg text-gray-300 mt-3 leading-relaxed max-w-lg">
{step.desc}
</p>
</div>
</div>
))}
</div>
</div>
{/* End spacer - shortened */}
<div className="h-[20vh]" />
</div>
</section>
);
};
export default Process;
</div>
{/* Content - positioned relative, scrolls over the fixed image */}
<div className="relative z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))]">
{/* Header - Static on mobile, fixed on desktop */}
<div className="relative mb-12 lg:mb-0 lg:fixed lg:top-1/2 lg:right-16 lg:-translate-y-1/2 z-20 text-left lg:text-right px-6 lg:px-0" style={{ clipPath: 'none' }}>
<span className="text-xs font-semibold uppercase tracking-widest text-gray-300 mb-2 block">Process</span>
<h2 className="font-display text-3xl lg:text-5xl font-medium text-white">
One consultation to begin,<br />
<span className="text-gray-400">three steps to clarity.</span>
</h2>
</div>
{/* Spacer for first screen - shortened */}
<div className="h-[30vh]" />
{/* Steps - LEFT side on desktop, full width on mobile */}
<div className="min-h-screen px-6 lg:px-16 py-24">
<div className="w-full lg:w-1/2 space-y-[60vh]">
{[
{ 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 bg-black/60 backdrop-blur-md p-8 rounded-2xl border border-white/10">
<div className="flex-shrink-0 mt-1">
<motion.span
whileHover={{ scale: 1.2, borderColor: "#3b82f6", color: "#3b82f6" }}
className="flex items-center justify-center w-12 h-12 rounded-xl border-2 border-white/30 text-lg font-bold text-white transition-colors"
>
{step.num}
</motion.span>
</div>
<div>
<h3 className="text-2xl lg:text-3xl font-medium text-white group-hover:translate-x-1 transition-transform group-hover:text-blue-400">{step.title}</h3>
<p className="text-lg text-gray-300 mt-3 leading-relaxed max-w-lg">
{step.desc}
</p>
</div>
</div>
))}
</div>
</div>
{/* End spacer - shortened */}
<div className="h-[20vh]" />
</div>
</section>
);
};
export default Process;

View File

@@ -7,14 +7,16 @@ interface SEOProps {
keywords?: string[];
canonicalUrl?: string;
schema?: object; // JSON-LD schema
ogImage?: string;
ogType?: string;
}
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema }) => {
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema, ogImage, ogType }) => {
useEffect(() => {
// Update Title
document.title = title;
// Helper to set meta tag
// Helper to set meta tag (name attribute)
const setMetaTag = (name: string, content: string) => {
let element = document.querySelector(`meta[name="${name}"]`);
if (!element) {
@@ -25,6 +27,17 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
element.setAttribute('content', content);
};
// Helper to set OG tag (property attribute)
const setOgTag = (property: string, content: string) => {
let element = document.querySelector(`meta[property="${property}"]`);
if (!element) {
element = document.createElement('meta');
element.setAttribute('property', property);
document.head.appendChild(element);
}
element.setAttribute('content', content);
};
// Update Meta Description
setMetaTag('description', description);
@@ -44,6 +57,19 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
link.setAttribute('href', canonicalUrl);
}
// Open Graph Tags
setOgTag('og:title', title);
setOgTag('og:description', description);
setOgTag('og:type', ogType || 'website');
setOgTag('og:site_name', 'Bay Area IT');
if (canonicalUrl) setOgTag('og:url', canonicalUrl);
if (ogImage) setOgTag('og:image', ogImage);
// Twitter Card Tags
setMetaTag('twitter:card', 'summary');
setMetaTag('twitter:title', title);
setMetaTag('twitter:description', description);
// Inject Schema
if (schema) {
const scriptId = 'seo-schema-script';
@@ -57,10 +83,7 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
script.textContent = JSON.stringify(schema);
}
// Cleanup function not strictly necessary for single page app navigation
// unless we want to remove specific tags on unmount, but usually we just overwrite them.
}, [title, description, keywords, canonicalUrl, schema]);
}, [title, description, keywords, canonicalUrl, schema, ogImage, ogType]);
return null;
};

View File

@@ -2,243 +2,222 @@ import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
import { Link } from 'react-router-dom';
gsap.registerPlugin(ScrollTrigger);
const servicesData = [
{
id: 1,
category: 'IT Infrastructure',
title: 'Windows 11 Transition',
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
icon: 'desktop_windows',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
category: 'Web Services',
title: 'Web Design',
description: 'Professional websites with domain registration and DNS support to give your business a clean, reliable online presence.',
icon: 'language',
image: '/assets/services/business-it.webp',
href: '/services/web-design-corpus-christi'
},
{
id: 2,
category: 'Web Services',
title: 'Web Services',
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
icon: 'language',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
title: 'Bay Area Email Services',
description: 'Enterprise cloud email with 99.99% uptime, local Texas support, 25 GB mailboxes, and business-grade delivery for $5 per inbox.',
icon: 'mail',
image: '/assets/services/business-it.webp',
href: '/services/business-email-services'
},
{
id: 3,
category: 'IT Infrastructure',
title: 'Performance Upgrades',
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
icon: 'speed',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
icon: 'print',
image: '/assets/services/printer-scanner.webp',
href: '/services'
},
{
id: 4,
category: 'IT Infrastructure',
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
icon: 'print',
image: '/assets/services/printer-scanner.png'
},
{
id: 5,
category: 'IT Infrastructure',
title: 'New/Refurbished Desktop Hardware',
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
icon: 'computer',
image: '/assets/services/desktop-hardware.png'
image: '/assets/services/desktop-hardware.webp',
href: '/services/computer-support'
},
{
id: 6,
category: 'Security',
title: 'VPN Setup',
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
icon: 'vpn_lock',
image: '/assets/services/vpn-setup.png'
},
{
id: 7,
id: 5,
category: 'Networking',
title: 'Network Infrastructure Support',
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
icon: 'lan',
image: '/assets/services/network-infrastructure.png'
image: '/assets/services/network-infrastructure.webp',
href: '/services'
},
{
id: 8,
id: 6,
category: 'Networking',
title: 'Network Attached Storage',
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
title: 'Shared Drive',
description: 'Setup and management of shared drive solutions so your team can store, access, and organize files reliably.',
icon: 'storage',
image: '/assets/services/nas-storage.png'
image: '/assets/services/nas-storage.webp',
href: '/services'
},
{
id: 9,
category: 'IT Infrastructure',
title: 'Business IT Support',
description: 'Comprehensive IT support for businesses, including help desk, maintenance, and strategic planning.',
icon: 'business_center',
image: '/assets/services/business-it.png'
},
{
id: 10,
id: 7,
category: 'IT Infrastructure',
title: 'IT Help Desk',
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
icon: 'support_agent',
image: '/assets/services/help-desk.png'
},
{
id: 11,
category: 'IT Infrastructure',
title: 'Managed IT Services',
description: 'Proactive monitoring, security, and management of your entire IT infrastructure for a fixed monthly fee.',
icon: 'admin_panel_settings',
image: '/assets/services/managed-it.png'
image: '/assets/services/help-desk.webp',
href: '/services/it-help-desk'
}
];
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking'];
interface ServicesProps {
preview?: boolean;
featuredIds?: number[];
}
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
const [activeCategory, setActiveCategory] = useState('All');
const [showAll, setShowAll] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Determine if we should be in "preview mode" (showing only a subset)
// This applies if preview is true OR if featuredIds are provided and we haven't clicked "Show More"
const isRestrictedView = (preview || featuredIds) && !showAll;
// Filter services based on category first (unless in restricted view with specific IDs, where we might want to ignore category or just show the specific ones)
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Networking'];
interface ServicesProps {
preview?: boolean;
featuredIds?: number[];
}
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
const [activeCategory, setActiveCategory] = useState('All');
const [showAll, setShowAll] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Determine if we should be in "preview mode" (showing only a subset)
// This applies if preview is true OR if featuredIds are provided and we haven't clicked "Show More"
const isRestrictedView = (preview || featuredIds) && !showAll;
// Filter services based on category first (unless in restricted view with specific IDs, where we might want to ignore category or just show the specific ones)
const filteredByCategory = activeCategory === 'All'
? servicesData
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
const displayedServices = useMemo(() => {
if (isRestrictedView) {
if (featuredIds && featuredIds.length > 0) {
// Sort the services to match the order of featuredIds
return featuredIds
.map(id => servicesData.find(s => s.id === id))
.filter((s): s is typeof servicesData[0] => s !== undefined);
}
// Fallback to first 3 if no IDs but preview is true
return servicesData.slice(0, 3);
}
// Show all (filtered by category)
return filteredByCategory;
}, [isRestrictedView, featuredIds, filteredByCategory]);
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
>
<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">Our Services</span>
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
Comprehensive IT solutions <span className="text-gray-400 dark:text-gray-600">tailored to your business needs.</span>
</h2>
</div>
{/* Categories - Hide in restricted view to keep it clean, or keep it? User said "mach nur das 3 services angezeigt werden". usually categories are for the full list. */}
{!isRestrictedView && (
<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>
)}
: servicesData.filter(s => s.category === activeCategory);
const displayedServices = useMemo(() => {
if (isRestrictedView) {
if (featuredIds && featuredIds.length > 0) {
// Sort the services to match the order of featuredIds
return featuredIds
.map(id => servicesData.find(s => s.id === id))
.filter((s): s is typeof servicesData[0] => s !== undefined);
}
// Fallback to first 3 if no IDs but preview is true
return servicesData.slice(0, 3);
}
// Show all (filtered by category)
return filteredByCategory;
}, [isRestrictedView, featuredIds, filteredByCategory]);
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 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
>
<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">Our Services</span>
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
Comprehensive IT solutions <span className="text-gray-400 dark:text-gray-600">tailored to your business needs.</span>
</h2>
</div>
{/* Categories - Hide in restricted view to keep it clean, or keep it? User said "mach nur das 3 services angezeigt werden". usually categories are for the full list. */}
{!isRestrictedView && (
<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">
{displayedServices.map((service) => (
{displayedServices.map((service, index) => (
<motion.div
key={service.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
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-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300"
>
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
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-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300"
>
{/* Image Container */}
<div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
<img
src={service.image}
alt={service.title}
loading={isRestrictedView ? 'lazy' : index < 3 ? 'eager' : 'lazy'}
decoding="async"
fetchPriority={isRestrictedView ? 'low' : index < 3 ? 'high' : 'low'}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
/>
<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-4 relative">
<motion.div
className="w-8 h-8 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-3 border border-gray-200 dark:border-white/10"
whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }}
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-lg 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-3">
{service.description}
</p>
<a href="/services" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
<div className="p-4 relative">
<motion.div
className="w-8 h-8 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-3 border border-gray-200 dark:border-white/10"
whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }}
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-lg 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-3">
{service.description}
</p>
<Link to={service.href} className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 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>
{isRestrictedView && (
<div className="mt-12 text-center">
<button
onClick={() => setShowAll(true)}
className="inline-flex items-center gap-2 px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors"
>
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
</button>
</div>
)}
{/* If we are showing all and originally had a restricted view, maybe show a "Show Less" but user didn't ask for it. The user said "then all are shown". */}
</div>
</motion.section>
);
};
export default Services;
</Link>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
{isRestrictedView && (
<div className="mt-12 text-center">
<button
onClick={() => setShowAll(true)}
className="inline-flex items-center gap-2 px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors"
>
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
</button>
</div>
)}
{/* If we are showing all and originally had a restricted view, maybe show a "Show Less" but user didn't ask for it. The user said "then all are shown". */}
</div>
</motion.section>
);
};
export default Services;

View File

@@ -23,7 +23,7 @@ const Testimonials: React.FC = () => {
</div>
<blockquote className="text-xl md:text-2xl font-medium leading-relaxed text-gray-900 dark:text-white mb-8 relative z-10">
"Bay Area Affiliates transformed our IT infrastructure completely. Their proactive approach means we rarely have downtime, and when issues do arise, they're resolved quickly. Our team can focus on patient care instead of tech problems."
"Bay Area IT transformed our IT infrastructure completely. Their proactive approach means we rarely have downtime, and when issues do arise, they're resolved quickly. Our team can focus on patient care instead of tech problems."
</blockquote>
<div className="flex items-center gap-4 relative z-10">