Neue services
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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
35
components/Breadcrumb.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
111
components/LoadingScreen.tsx
Normal file
111
components/LoadingScreen.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user