Complete SEO overhaul

This commit is contained in:
Timo Knuth 2026-01-22 15:17:20 +01:00
parent e5c5503e08
commit 42e0971a13
37 changed files with 4319 additions and 1217 deletions

31
App.tsx
View File

@ -12,6 +12,10 @@ import AboutPage from './src/pages/AboutPage';
import ServicesPage from './src/pages/ServicesPage';
import BlogPage from './src/pages/BlogPage';
import ContactPage from './src/pages/ContactPage';
import LocationPage from './src/pages/LocationPage';
import ServicePage from './src/pages/ServicePage';
import BlogPostPage from './src/pages/BlogPostPage';
import { locationData, serviceData, blogPostData } from './src/data/seoData';
// Register GSAP plugins globally
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
@ -69,6 +73,33 @@ const AppContent: React.FC = () => {
<Route path="/services" element={<ServicesPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/contact" element={<ContactPage />} />
{/* SEO Location Pages */}
{locationData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<LocationPage data={data} />}
/>
))}
{/* SEO Service Pages */}
{serviceData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<ServicePage data={data} />}
/>
))}
{/* Authority Blog Posts */}
{blogPostData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<BlogPostPage data={data} />}
/>
))}
</Routes>
</main>
<Footer />

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { locationData } from '../src/data/seoData';
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">
<div className="max-w-4xl mx-auto">
<h2 className="font-display text-3xl md:text-4xl font-bold mb-6 text-gray-900 dark:text-white">
Areas We Serve Local IT Support Across the Coastal Bend
</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.
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-12">
{locationData.map((loc) => (
<Link
key={loc.slug}
to={`/${loc.slug}`}
className="p-4 bg-white dark:bg-white/10 rounded-lg shadow-sm hover:shadow-md transition-all text-gray-800 dark:text-gray-200 font-medium hover:text-black dark:hover:text-white"
>
{loc.city}
</Link>
))}
</div>
<p className="text-lg text-gray-600 dark:text-gray-400">
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>
</div>
</div>
</section>
);
};
export default AreasWeServe;

View File

@ -1,74 +1,74 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
const CTA: React.FC = () => {
return (
<section className="py-24 px-6 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-4xl mx-auto text-center">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="font-display text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white"
>
Ready for <span className="text-gray-400 dark:text-gray-500">reliable IT?</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
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.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
>
<Link
to="/contact"
className="px-8 py-4 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium transition-all hover:scale-105 shadow-lg w-full sm:w-auto"
>
Book a 20-minute assessment
</Link>
<Link
to="/contact"
className="px-8 py-4 bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white rounded-full font-medium transition-all hover:bg-gray-200 dark:hover:bg-white/20 w-full sm:w-auto"
>
Send a message
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="mt-16 grid md:grid-cols-3 gap-8 text-left"
>
<div className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10">
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">How quickly can you start?</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">Most assessments can begin within 48 hours of contact.</p>
</div>
<div className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10">
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">How do you price services?</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">Transparent monthly pricing based on devices and services needed.</p>
</div>
<div className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10">
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">What's included in support?</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">24/7 monitoring, helpdesk, proactive maintenance, and SLA guarantees.</p>
</div>
</motion.div>
</div>
</section>
);
};
export default CTA;
import React from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
const CTA: React.FC = () => {
return (
<section className="py-24 px-6 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-4xl mx-auto text-center">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="font-display text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white"
>
Ready for <span className="text-gray-400 dark:text-gray-500">reliable IT?</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
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.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
>
<Link
to="/contact"
className="px-8 py-4 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium transition-all hover:scale-105 shadow-lg w-full sm:w-auto"
>
Book a 20-minute assessment
</Link>
<Link
to="/contact"
className="px-8 py-4 bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white rounded-full font-medium transition-all hover:bg-gray-200 dark:hover:bg-white/20 w-full sm:w-auto"
>
Send a message
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="mt-16 grid md:grid-cols-3 gap-8 text-left"
>
<div className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10">
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">How quickly can you start?</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">Most assessments can begin within 48 hours of contact.</p>
</div>
<div className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10">
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">How do you price services?</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">Transparent monthly pricing based on devices and services needed.</p>
</div>
<div className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10">
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">What's included in support?</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">24/7 monitoring, helpdesk, proactive maintenance, and SLA guarantees.</p>
</div>
</motion.div>
</div>
</section>
);
};
export default CTA;

View File

@ -1,28 +1,28 @@
import React, { useEffect, useRef } from 'react';
import { motion, useInView, useSpring, useTransform } from 'framer-motion';
interface CounterProps {
value: number;
}
const Counter: React.FC<CounterProps> = ({ value }) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-20%" });
// Using slow/heavy physics as requested for premium feel
const spring = useSpring(0, { mass: 3, stiffness: 75, damping: 30 });
const display = useTransform(spring, (current) =>
// formatting: if decimal exists in target, show 1 decimal, else integer
value % 1 !== 0 ? current.toFixed(1) : Math.round(current).toLocaleString()
);
useEffect(() => {
if (isInView) {
spring.set(value);
}
}, [isInView, value, spring]);
return <motion.span ref={ref}>{display}</motion.span>;
};
export default Counter;
import React, { useEffect, useRef } from 'react';
import { motion, useInView, useSpring, useTransform } from 'framer-motion';
interface CounterProps {
value: number;
}
const Counter: React.FC<CounterProps> = ({ value }) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-20%" });
// Using slow/heavy physics as requested for premium feel
const spring = useSpring(0, { mass: 3, stiffness: 75, damping: 30 });
const display = useTransform(spring, (current) =>
// formatting: if decimal exists in target, show 1 decimal, else integer
value % 1 !== 0 ? current.toFixed(1) : Math.round(current).toLocaleString()
);
useEffect(() => {
if (isInView) {
spring.set(value);
}
}, [isInView, value, spring]);
return <motion.span ref={ref}>{display}</motion.span>;
};
export default Counter;

65
components/FAQ.tsx Normal file
View File

@ -0,0 +1,65 @@
import React from 'react';
import { motion } from 'framer-motion';
import { FAQItem } from '../src/data/seoData';
interface FAQProps {
items: FAQItem[];
}
const FAQ: React.FC<FAQProps> = ({ items }) => {
if (!items || items.length === 0) return null;
return (
<section className="py-24 px-6 bg-white dark:bg-black">
<div className="max-w-3xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="font-display text-3xl md:text-4xl font-bold mb-4 text-gray-900 dark:text-white">
Frequently Asked Questions
</h2>
<p className="text-gray-600 dark:text-gray-400">
Common questions about our IT services.
</p>
</motion.div>
<div className="space-y-6">
{items.map((faq, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10"
>
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">{faq.question}</h3>
<p className="text-gray-600 dark:text-gray-300">{faq.answer}</p>
</motion.div>
))}
</div>
</div>
{/* JSON-LD for FAQ Page */}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": items.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
})
}} />
</section>
);
};
export default FAQ;

68
components/SEO.tsx Normal file
View File

@ -0,0 +1,68 @@
import React, { useEffect } from 'react';
interface SEOProps {
title: string;
description: string;
keywords?: string[];
canonicalUrl?: string;
schema?: object; // JSON-LD schema
}
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema }) => {
useEffect(() => {
// Update Title
document.title = title;
// Helper to set meta tag
const setMetaTag = (name: string, content: string) => {
let element = document.querySelector(`meta[name="${name}"]`);
if (!element) {
element = document.createElement('meta');
element.setAttribute('name', name);
document.head.appendChild(element);
}
element.setAttribute('content', content);
};
// Update Meta Description
setMetaTag('description', description);
// Update Keywords
if (keywords && keywords.length > 0) {
setMetaTag('keywords', keywords.join(', '));
}
// Update Canonical
if (canonicalUrl) {
let link = document.querySelector('link[rel="canonical"]');
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', 'canonical');
document.head.appendChild(link);
}
link.setAttribute('href', canonicalUrl);
}
// Inject Schema
if (schema) {
const scriptId = 'seo-schema-script';
let script = document.getElementById(scriptId);
if (!script) {
script = document.createElement('script');
script.id = scriptId;
script.setAttribute('type', 'application/ld+json');
document.head.appendChild(script);
}
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]);
return null;
};
export default SEO;

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useLayoutEffect } from 'react';
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
@ -69,45 +69,68 @@ const servicesData = [
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
icon: 'storage',
image: '/assets/services/nas-storage.png'
},
{
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,
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'
}
];
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking'];
const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
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);
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
// Reset refs on render to handle filtering updates
imagesRef.current = [];
// 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;
const filteredServices = activeCategory === 'All'
// 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 = preview ? servicesData.slice(0, 3) : filteredServices;
useLayoutEffect(() => {
const ctx = gsap.context(() => {
imagesRef.current.forEach((imgWrapper) => {
if (!imgWrapper) return;
gsap.to(imgWrapper, {
yPercent: 30,
ease: "none",
scrollTrigger: {
trigger: imgWrapper.closest('.group'),
start: "top bottom",
end: "bottom top",
scrub: true
}
});
});
}, containerRef);
return () => ctx.revert();
}, [filteredServices]);
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
@ -127,32 +150,35 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
</h2>
</div>
<div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
? 'text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
}`}
>
{cat}
{activeCategory === cat && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
/>
)}
</button>
))}
</div>
{/* 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">
{filteredServices.map((service, index) => (
{displayedServices.map((service) => (
<motion.div
key={service.id}
layout
@ -164,34 +190,28 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
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-64 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
{/* Parallax Wrapper */}
<div
ref={el => { if (el) imagesRef.current.push(el); }}
className="w-full h-[140%] -mt-[20%]"
>
<img
src={service.image}
alt={service.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
/>
</div>
<div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
<img
src={service.image}
alt={service.title}
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-6 relative">
<div className="p-4 relative">
<motion.div
className="w-10 h-10 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-4 border border-gray-200 dark:border-white/10"
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-xl font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-4">
<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="#" 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">
<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">
Learn More <motion.span
className="material-symbols-outlined text-xs ml-1"
animate={{ x: [0, 5, 0] }}
@ -204,16 +224,18 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
</AnimatePresence>
</div>
{preview && (
{isRestrictedView && (
<div className="mt-12 text-center">
<a
href="/services"
<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"
>
View all services <span className="material-symbols-outlined text-sm">arrow_forward</span>
</a>
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>
);

View File

@ -1,44 +1,44 @@
import React from 'react';
import { motion } from 'framer-motion';
const Testimonials: React.FC = () => {
return (
<section className="py-24 px-6 bg-background-light dark:bg-background-dark relative overflow-hidden 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-5xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="bg-white dark:bg-white/5 backdrop-blur-sm p-8 md:p-12 rounded-3xl border border-gray-200 dark:border-white/10 shadow-2xl relative"
>
{/* Quote Icon */}
<div className="absolute top-8 right-8 text-blue-100 dark:text-white/5 select-none">
<span className="material-symbols-outlined text-8xl">format_quote</span>
</div>
<div className="flex text-yellow-400 mb-6 gap-1 relative z-10">
{[1, 2, 3, 4, 5].map((star) => (
<span key={star} className="material-symbols-outlined fill-current">star</span>
))}
</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."
</blockquote>
<div className="flex items-center gap-4 relative z-10">
<div className="w-12 h-12 bg-black dark:bg-white rounded-full flex items-center justify-center text-white dark:text-black font-bold text-lg">
SM
</div>
<div>
<div className="font-bold text-gray-900 dark:text-white">Sarah Martinez</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Operations Manager, Coastal Medical Group</div>
</div>
</div>
</motion.div>
</div>
</section>
);
};
export default Testimonials;
import React from 'react';
import { motion } from 'framer-motion';
const Testimonials: React.FC = () => {
return (
<section className="py-24 px-6 bg-background-light dark:bg-background-dark relative overflow-hidden 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-5xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="bg-white dark:bg-white/5 backdrop-blur-sm p-8 md:p-12 rounded-3xl border border-gray-200 dark:border-white/10 shadow-2xl relative"
>
{/* Quote Icon */}
<div className="absolute top-8 right-8 text-blue-100 dark:text-white/5 select-none">
<span className="material-symbols-outlined text-8xl">format_quote</span>
</div>
<div className="flex text-yellow-400 mb-6 gap-1 relative z-10">
{[1, 2, 3, 4, 5].map((star) => (
<span key={star} className="material-symbols-outlined fill-current">star</span>
))}
</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."
</blockquote>
<div className="flex items-center gap-4 relative z-10">
<div className="w-12 h-12 bg-black dark:bg-white rounded-full flex items-center justify-center text-white dark:text-black font-bold text-lg">
SM
</div>
<div>
<div className="font-bold text-gray-900 dark:text-white">Sarah Martinez</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Operations Manager, Coastal Medical Group</div>
</div>
</div>
</motion.div>
</div>
</section>
);
};
export default Testimonials;

View File

@ -65,6 +65,22 @@
.lenis.lenis-scrolling iframe {
pointer-events: none;
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
<body class="bg-background-light dark:bg-background-dark text-gray-900 dark:text-white font-sans antialiased selection:bg-white selection:text-black transition-colors duration-300">
<div id="root"></div>

528
package-lock.json generated
View File

@ -18,6 +18,7 @@
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
@ -1475,6 +1476,19 @@
"node": ">=6.9.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/gsap": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
@ -1690,6 +1704,16 @@
"react-dom": ">=18"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/rollup": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
@ -1790,6 +1814,510 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",

View File

@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"generate:seo": "npx tsx scripts/generate-sitemap.ts && npx tsx scripts/generate-robots.ts"
},
"dependencies": {
"@studio-freight/lenis": "^1.0.42",
@ -19,6 +20,7 @@
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

6
public/robots.txt Normal file
View File

@ -0,0 +1,6 @@
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Sitemap: https://bayareait.services/sitemap.xml

147
public/sitemap.xml Normal file
View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://bayareait.services</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://bayareait.services/services</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/contact</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/about</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-portland-tx</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-rockport-tx</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-aransas-pass-tx</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-kingsville-tx</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/business-it-support</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-help-desk</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/computer-support</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/managed-it-services-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-small-business-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/outsourced-it-support-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-service-vs-inhouse-it</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/common-it-problems-businesses-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-cost-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-corpus-christi-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-portland-tx-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-rockport-tx-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-aransas-pass-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-kingsville-tx-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

View File

@ -0,0 +1,28 @@
import fs from 'fs';
import path from 'path';
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
const generateRobots = () => {
const content = `User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Sitemap: ${BASE_URL}/sitemap.xml
`;
return content;
};
const robots = generateRobots();
const outputPath = path.resolve(process.cwd(), 'public/robots.txt');
// Ensure public directory exists
const publicDir = path.dirname(outputPath);
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
fs.writeFileSync(outputPath, robots);
console.log(`✅ Robots.txt generated at ${outputPath}`);

View File

@ -0,0 +1,85 @@
import fs from 'fs';
import path from 'path';
import { locationData, serviceData, blogPostData } from '../src/data/seoData';
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
/**
* Generates the sitemap.xml content
*/
const generateSitemap = () => {
const currentDate = new Date().toISOString().split('T')[0];
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
`;
// Static Pages
const staticPages = [
'',
'/services',
'/blog',
'/contact',
'/about'
];
staticPages.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}${page}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>monthly</changefreq>
<priority>${page === '' ? '1.0' : '0.8'}</priority>
</url>
`;
});
// Location Pages
locationData.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}/${page.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
`;
});
// Service Pages
serviceData.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}/${page.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
`;
});
// Blog Posts
blogPostData.forEach(post => {
xml += ` <url>
<loc>${BASE_URL}/blog/${post.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
`;
});
xml += `</urlset>`;
return xml;
};
// Write to public/sitemap.xml
const sitemap = generateSitemap();
const outputPath = path.resolve(process.cwd(), 'public/sitemap.xml');
// Ensure public directory exists
const publicDir = path.dirname(outputPath);
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
fs.writeFileSync(outputPath, sitemap);
console.log(`✅ Sitemap generated at ${outputPath}`);

1251
src/data/seoData.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,246 +1,246 @@
import React, { useEffect, useRef, useState } from 'react';
import { motion, useInView, useSpring, useTransform, useScroll, useMotionValueEvent } from 'framer-motion';
import Contact from '../../components/Contact';
const Counter = ({ value }: { value: number }) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-20%" });
const spring = useSpring(0, { mass: 3, stiffness: 75, damping: 30 });
const display = useTransform(spring, (current) =>
value % 1 !== 0 ? current.toFixed(1) : Math.round(current).toLocaleString()
);
useEffect(() => {
if (isInView) {
spring.set(value);
}
}, [isInView, value, spring]);
return <motion.span ref={ref}>{display}</motion.span>;
};
const AboutPage: React.FC = () => {
const timelineRef = useRef<HTMLDivElement>(null);
const [activeTimelineIndex, setActiveTimelineIndex] = useState(0);
const { scrollYProgress } = useScroll({
target: timelineRef,
offset: ["start end", "end center"]
});
const heightTransform = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
useMotionValueEvent(scrollYProgress, "change", (latest) => {
// Calculate index based on scroll progress with a slight offset to trigger earlier
const index = Math.floor(latest * 4.5); // 4 items, multiplier slightly > 4 ensures last one gets hit
setActiveTimelineIndex(Math.min(index, 3));
});
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const stats = [
{ label: 'Businesses served', value: '150+' },
{ label: 'Uptime achieved', value: '99.9%' },
{ label: 'Years of service', value: '15+' },
{ label: 'Response time', value: '<2min' },
];
const values = [
{
title: 'Security-First',
desc: 'Every solution we implement prioritizes your data security and business continuity.',
icon: 'security'
},
{
title: 'Reliability',
desc: 'We build systems that work consistently, so you can depend on your technology.',
icon: 'verified'
},
{
title: 'Clarity',
desc: 'No tech jargon or hidden fees. We explain what we do and why it matters.',
icon: 'visibility'
}
];
const timeline = [
{ year: '2010', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
{ year: '2015', title: 'Expanded Service Portfolio', desc: 'Added cloud services and advanced networking to serve growing businesses.' },
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped 100+ businesses transition to secure remote work during the pandemic.' },
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now serving 150+ businesses with modern, reliable IT infrastructure.' },
];
return (
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
{/* Hero Section */}
<section className="relative py-20 px-6 overflow-hidden">
<div className="max-w-7xl mx-auto text-center relative z-10">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="font-display text-4xl md:text-6xl font-bold mb-6 text-gray-900 dark:text-white"
>
Local IT expertise for the <br /><span className="text-gray-500 dark:text-gray-400">Coastal Bend</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"
>
Since 2010, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that drive growth.
</motion.p>
</div>
</section>
{/* Our Story */}
<section className="py-20 px-6 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="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
<div className="max-w-4xl mx-auto relative z-10">
<h2 className="font-display text-3xl font-bold mb-8 text-gray-900 dark:text-white">Our Story</h2>
<div className="prose dark:prose-invert max-w-none text-lg text-gray-600 dark:text-gray-300 space-y-6">
<p>
Bay Area Affiliates was founded with a simple belief: local businesses deserve the same level of IT expertise and reliability as large corporations, but with the personal touch that only comes from working with your neighbors.
</p>
<p>
Over the years, we've watched the Coastal Bend grow and change. We've helped businesses navigate technology challenges, from the transition to cloud computing to the rapid shift to remote work. Through it all, we've maintained our commitment to clear communication, reliable solutions, and exceptional service.
</p>
<p>
Today, we're proud to serve over 150 businesses across the region, from Corpus Christi to the smallest coastal communities. Our team combines deep technical expertise with real-world business understanding to deliver IT solutions that actually work for our clients.
</p>
</div>
</div>
</section>
{/* Stats */}
<section className="py-16 px-6 bg-white/5 backdrop-blur-sm border-y border-white/5 text-white relative">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))] pointer-events-none"></div>
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-center relative z-10">
{[
{ label: 'Businesses served', value: 150, suffix: '+' },
{ label: 'Uptime achieved', value: 99.9, suffix: '%' },
{ label: 'Years of service', value: 15, suffix: '+' },
{ label: 'Response time', value: 2, prefix: '<', suffix: 'min' },
].map((stat, index) => (
<div key={index} className="p-4">
<div className="text-4xl md:text-5xl font-bold mb-2 flex justify-center items-center gap-1">
{stat.prefix && <span>{stat.prefix}</span>}
<Counter value={stat.value} />
{stat.suffix && <span>{stat.suffix}</span>}
</div>
<div className="text-gray-400 font-medium">{stat.label}</div>
</div>
))}
</div>
</section>
{/* Values */}
<section className="py-24 px-6 bg-gray-50 dark:bg-black/20 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))] pointer-events-none"></div>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-blue-500/5 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
<div className="max-w-7xl mx-auto relative z-10">
<div className="text-center mb-16">
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">Our Values</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{values.map((value, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
whileHover={{ y: -5 }}
className="bg-white dark:bg-[#161616] p-8 rounded-2xl shadow-lg border border-gray-100 dark:border-white/5"
>
<div className="w-12 h-12 bg-gray-100 dark:bg-white/10 rounded-xl flex items-center justify-center mb-6 text-gray-900 dark:text-white">
<span className="material-symbols-outlined text-2xl">{value.icon}</span>
</div>
<h3 className="text-xl font-bold mb-3 text-gray-900 dark:text-white">{value.title}</h3>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{value.desc}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Timeline */}
<section className="py-24 px-6 relative" ref={timelineRef}>
<div className="absolute inset-0 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))] pointer-events-none"></div>
<div className="max-w-4xl mx-auto relative z-10">
<h2 className="font-display text-3xl font-bold mb-12 text-center text-gray-900 dark:text-white">Our Journey</h2>
<div className="space-y-12 relative">
{/* Base Line */}
<div className="absolute left-[19px] md:left-1/2 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-white/10 -translate-x-1/2 md:translate-x-0"></div>
{/* Light Beam Line */}
<motion.div
style={{ height: heightTransform }}
className="absolute left-[19px] md:left-1/2 top-[-60px] w-0.5 bg-gradient-to-b from-black via-black to-transparent dark:from-white dark:via-white dark:to-transparent origin-top shadow-[0_0_25px_2px_rgba(0,0,0,0.5)] dark:shadow-[0_0_25px_2px_rgba(255,255,255,0.7)] -translate-x-1/2 md:translate-x-0 z-10"
></motion.div>
{timeline.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: index % 2 === 0 ? -50 : 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className={`relative flex flex-col md:flex-row gap-8 ${index % 2 === 0 ? 'md:text-right' : 'md:flex-row-reverse md:text-left'}`}
>
<div className="md:w-1/2 pt-1">
<span className="inline-block px-4 py-1 rounded-full bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white font-bold text-sm mb-3">
{item.year}
</span>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{item.title}</h3>
<p className="text-gray-600 dark:text-gray-400">{item.desc}</p>
</div>
{/* Timeline Dot */}
<div className="absolute left-0 md:left-1/2 -translate-x-[5px] md:-translate-x-1/2 w-10 h-10 flex items-center justify-center z-20">
<motion.div
className="w-4 h-4 rounded-full border-2 transition-colors duration-500"
animate={index <= activeTimelineIndex ? {
backgroundColor: "#000000", // Will be white in dark mode via class if needed, checking global styles
scale: 1.5,
borderColor: "#000000",
boxShadow: "0 0 20px rgba(0,0,0,0.5)"
} : {
backgroundColor: "transparent",
scale: 1,
borderColor: "rgba(156, 163, 175, 0.5)", // gray-400
boxShadow: "none"
}}
style={{
// Manual dark mode override since animate doesn't support class names easily without variants
// Using CSS variable or just forcing white for dark mode if we knew the context,
// but better to rely on simpler style.
// We will assume 'bg-black dark:bg-white' equivalent behavior is desired.
// To do this simply, we'll set colors that work on both or use a conditional standard div.
// Let's use standard classes for the base and animate scale/shadow.
}}
>
<div className={`w-full h-full rounded-full ${index <= activeTimelineIndex ? 'bg-black dark:bg-white' : 'bg-gray-200 dark:bg-white/20'}`} />
</motion.div>
</div>
<div className="md:w-1/2"></div>
</motion.div>
))}
</div>
</div>
</section>
<Contact />
</div>
);
};
export default AboutPage;
import React, { useEffect, useRef, useState } from 'react';
import { motion, useInView, useSpring, useTransform, useScroll, useMotionValueEvent } from 'framer-motion';
import Contact from '../../components/Contact';
const Counter = ({ value }: { value: number }) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-20%" });
const spring = useSpring(0, { mass: 3, stiffness: 75, damping: 30 });
const display = useTransform(spring, (current) =>
value % 1 !== 0 ? current.toFixed(1) : Math.round(current).toLocaleString()
);
useEffect(() => {
if (isInView) {
spring.set(value);
}
}, [isInView, value, spring]);
return <motion.span ref={ref}>{display}</motion.span>;
};
const AboutPage: React.FC = () => {
const timelineRef = useRef<HTMLDivElement>(null);
const [activeTimelineIndex, setActiveTimelineIndex] = useState(0);
const { scrollYProgress } = useScroll({
target: timelineRef,
offset: ["start end", "end center"]
});
const heightTransform = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
useMotionValueEvent(scrollYProgress, "change", (latest) => {
// Calculate index based on scroll progress with a slight offset to trigger earlier
const index = Math.floor(latest * 4.5); // 4 items, multiplier slightly > 4 ensures last one gets hit
setActiveTimelineIndex(Math.min(index, 3));
});
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const stats = [
{ label: 'Businesses served', value: '150+' },
{ label: 'Uptime achieved', value: '99.9%' },
{ label: 'Years of service', value: '15+' },
{ label: 'Response time', value: '<2min' },
];
const values = [
{
title: 'Security-First',
desc: 'Every solution we implement prioritizes your data security and business continuity.',
icon: 'security'
},
{
title: 'Reliability',
desc: 'We build systems that work consistently, so you can depend on your technology.',
icon: 'verified'
},
{
title: 'Clarity',
desc: 'No tech jargon or hidden fees. We explain what we do and why it matters.',
icon: 'visibility'
}
];
const timeline = [
{ year: '2010', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
{ year: '2015', title: 'Expanded Service Portfolio', desc: 'Added cloud services and advanced networking to serve growing businesses.' },
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped 100+ businesses transition to secure remote work during the pandemic.' },
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now serving 150+ businesses with modern, reliable IT infrastructure.' },
];
return (
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
{/* Hero Section */}
<section className="relative py-20 px-6 overflow-hidden">
<div className="max-w-7xl mx-auto text-center relative z-10">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="font-display text-4xl md:text-6xl font-bold mb-6 text-gray-900 dark:text-white"
>
Local IT expertise for the <br /><span className="text-gray-500 dark:text-gray-400">Coastal Bend</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"
>
Since 2010, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that drive growth.
</motion.p>
</div>
</section>
{/* Our Story */}
<section className="py-20 px-6 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="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
<div className="max-w-4xl mx-auto relative z-10">
<h2 className="font-display text-3xl font-bold mb-8 text-gray-900 dark:text-white">Our Story</h2>
<div className="prose dark:prose-invert max-w-none text-lg text-gray-600 dark:text-gray-300 space-y-6">
<p>
Bay Area Affiliates was founded with a simple belief: local businesses deserve the same level of IT expertise and reliability as large corporations, but with the personal touch that only comes from working with your neighbors.
</p>
<p>
Over the years, we've watched the Coastal Bend grow and change. We've helped businesses navigate technology challenges, from the transition to cloud computing to the rapid shift to remote work. Through it all, we've maintained our commitment to clear communication, reliable solutions, and exceptional service.
</p>
<p>
Today, we're proud to serve over 150 businesses across the region, from Corpus Christi to the smallest coastal communities. Our team combines deep technical expertise with real-world business understanding to deliver IT solutions that actually work for our clients.
</p>
</div>
</div>
</section>
{/* Stats */}
<section className="py-16 px-6 bg-white/5 backdrop-blur-sm border-y border-white/5 text-white relative">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))] pointer-events-none"></div>
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-center relative z-10">
{[
{ label: 'Businesses served', value: 150, suffix: '+' },
{ label: 'Uptime achieved', value: 99.9, suffix: '%' },
{ label: 'Years of service', value: 15, suffix: '+' },
{ label: 'Response time', value: 2, prefix: '<', suffix: 'min' },
].map((stat, index) => (
<div key={index} className="p-4">
<div className="text-4xl md:text-5xl font-bold mb-2 flex justify-center items-center gap-1">
{stat.prefix && <span>{stat.prefix}</span>}
<Counter value={stat.value} />
{stat.suffix && <span>{stat.suffix}</span>}
</div>
<div className="text-gray-400 font-medium">{stat.label}</div>
</div>
))}
</div>
</section>
{/* Values */}
<section className="py-24 px-6 bg-gray-50 dark:bg-black/20 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))] pointer-events-none"></div>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-blue-500/5 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
<div className="max-w-7xl mx-auto relative z-10">
<div className="text-center mb-16">
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">Our Values</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{values.map((value, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
whileHover={{ y: -5 }}
className="bg-white dark:bg-[#161616] p-8 rounded-2xl shadow-lg border border-gray-100 dark:border-white/5"
>
<div className="w-12 h-12 bg-gray-100 dark:bg-white/10 rounded-xl flex items-center justify-center mb-6 text-gray-900 dark:text-white">
<span className="material-symbols-outlined text-2xl">{value.icon}</span>
</div>
<h3 className="text-xl font-bold mb-3 text-gray-900 dark:text-white">{value.title}</h3>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{value.desc}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Timeline */}
<section className="py-24 px-6 relative" ref={timelineRef}>
<div className="absolute inset-0 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))] pointer-events-none"></div>
<div className="max-w-4xl mx-auto relative z-10">
<h2 className="font-display text-3xl font-bold mb-12 text-center text-gray-900 dark:text-white">Our Journey</h2>
<div className="space-y-12 relative">
{/* Base Line */}
<div className="absolute left-[19px] md:left-1/2 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-white/10 -translate-x-1/2 md:translate-x-0"></div>
{/* Light Beam Line */}
<motion.div
style={{ height: heightTransform }}
className="absolute left-[19px] md:left-1/2 top-[-60px] w-0.5 bg-gradient-to-b from-black via-black to-transparent dark:from-white dark:via-white dark:to-transparent origin-top shadow-[0_0_25px_2px_rgba(0,0,0,0.5)] dark:shadow-[0_0_25px_2px_rgba(255,255,255,0.7)] -translate-x-1/2 md:translate-x-0 z-10"
></motion.div>
{timeline.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: index % 2 === 0 ? -50 : 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className={`relative flex flex-col md:flex-row gap-8 ${index % 2 === 0 ? 'md:text-right' : 'md:flex-row-reverse md:text-left'}`}
>
<div className="md:w-1/2 pt-1">
<span className="inline-block px-4 py-1 rounded-full bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white font-bold text-sm mb-3">
{item.year}
</span>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{item.title}</h3>
<p className="text-gray-600 dark:text-gray-400">{item.desc}</p>
</div>
{/* Timeline Dot */}
<div className="absolute left-0 md:left-1/2 -translate-x-[5px] md:-translate-x-1/2 w-10 h-10 flex items-center justify-center z-20">
<motion.div
className="w-4 h-4 rounded-full border-2 transition-colors duration-500"
animate={index <= activeTimelineIndex ? {
backgroundColor: "#000000", // Will be white in dark mode via class if needed, checking global styles
scale: 1.5,
borderColor: "#000000",
boxShadow: "0 0 20px rgba(0,0,0,0.5)"
} : {
backgroundColor: "transparent",
scale: 1,
borderColor: "rgba(156, 163, 175, 0.5)", // gray-400
boxShadow: "none"
}}
style={{
// Manual dark mode override since animate doesn't support class names easily without variants
// Using CSS variable or just forcing white for dark mode if we knew the context,
// but better to rely on simpler style.
// We will assume 'bg-black dark:bg-white' equivalent behavior is desired.
// To do this simply, we'll set colors that work on both or use a conditional standard div.
// Let's use standard classes for the base and animate scale/shadow.
}}
>
<div className={`w-full h-full rounded-full ${index <= activeTimelineIndex ? 'bg-black dark:bg-white' : 'bg-gray-200 dark:bg-white/20'}`} />
</motion.div>
</div>
<div className="md:w-1/2"></div>
</motion.div>
))}
</div>
</div>
</section>
<Contact />
</div>
);
};
export default AboutPage;

View File

@ -1,104 +1,81 @@
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import Contact from '../../components/Contact';
const posts = [
{
id: 1,
title: 'Upgrade your HDD to SSD for a big speed boost',
excerpt: 'A practical checklist for Corpus Christi business owners considering SSD upgrades, including before/after performance comparisons and cost analysis.',
image: '/assets/services/desktop-hardware.png',
category: 'Hardware',
readTime: '5 min read',
date: 'Jan 15, 2026'
},
{
id: 2,
title: 'Secure your corporate network access with WireGuard VPN',
excerpt: 'Learn why Corpus Christi businesses are switching to WireGuard VPN for faster, more secure remote access, and how to implement it properly in the Coastal Bend.',
image: '/assets/services/vpn-setup.png',
category: 'Security',
readTime: '7 min read',
date: 'Jan 10, 2026'
},
{
id: 3,
title: 'What comprehensive IT support looks like for SMBs',
excerpt: 'Understanding the full scope of managed IT services for Corpus Christi small businesses: from hardware and network infrastructure to virtualization and helpdesk support.',
image: '/assets/services/network-infrastructure.png',
category: 'Strategy',
readTime: '6 min read',
date: 'Jan 05, 2026'
}
];
const BlogPage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute top-[300px] left-1/2 -translate-x-1/2 w-[800px] h-[800px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
<section className="py-20 px-6 bg-white dark:bg-[#0f0f0f] border-b border-gray-100 dark:border-white/5 relative bg-transparent">
<div className="max-w-4xl mx-auto text-center relative z-10">
<span className="text-blue-600 dark:text-blue-400 font-bold tracking-widest uppercase text-sm mb-3 block">Latest Insights</span>
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white">
Tech insights for the <br /><span className="text-gray-400">Coastal Bend business.</span>
</h1>
</div>
</section>
<section className="py-16 px-6 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-5xl mx-auto space-y-16">
{posts.map((post) => (
<motion.div
key={post.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
whileHover={{ y: -5 }}
className="group grid md:grid-cols-2 gap-0 bg-white dark:bg-[#161616] rounded-3xl overflow-hidden shadow-lg border border-gray-100 dark:border-white/5 hover:shadow-2xl hover:shadow-blue-900/10 transition-all duration-300"
>
<div className="h-64 md:h-auto overflow-hidden relative">
<img
src={post.image}
alt={post.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-black/70 backdrop-blur-md text-white text-xs font-bold rounded-full border border-white/20">
{post.category}
</span>
</div>
</div>
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
<span>{post.date}</span>
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
<span>{post.readTime}</span>
</div>
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{post.title}
</h2>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-8">
{post.excerpt}
</p>
<div className="mt-auto">
<span className="inline-flex items-center gap-2 font-bold text-blue-600 dark:text-white group-hover:gap-3 transition-all">
Read article <span className="material-symbols-outlined text-sm">arrow_forward</span>
</span>
</div>
</div>
</motion.div>
))}
</div>
</section>
<Contact />
</div>
);
};
export default BlogPage;
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import Contact from '../../components/Contact';
import { blogPostData } from '../data/seoData';
const BlogPage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute top-[300px] left-1/2 -translate-x-1/2 w-[800px] h-[800px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
<section className="py-20 px-6 bg-white dark:bg-[#0f0f0f] border-b border-gray-100 dark:border-white/5 relative bg-transparent">
<div className="max-w-4xl mx-auto text-center relative z-10">
<span className="text-blue-600 dark:text-blue-400 font-bold tracking-widest uppercase text-sm mb-3 block">Latest Insights</span>
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white">
Tech insights for the <br /><span className="text-gray-400">Coastal Bend business.</span>
</h1>
</div>
</section>
<section className="py-16 px-6 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-5xl mx-auto space-y-16">
{blogPostData.map((post) => (
<Link
key={post.id}
to={`/${post.slug}`}
className="block"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
whileHover={{ y: -5 }}
className="group grid md:grid-cols-2 gap-0 bg-white dark:bg-[#161616] rounded-3xl overflow-hidden shadow-lg border border-gray-100 dark:border-white/5 hover:shadow-2xl hover:shadow-blue-900/10 transition-all duration-300"
>
<div className="h-64 md:h-auto overflow-hidden relative">
<img
src={post.image || '/images/blog/default.png'}
alt={post.h1}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-black/70 backdrop-blur-md text-white text-xs font-bold rounded-full border border-white/20">
{post.category === 'authority' ? 'IT Insights' : 'Local Services'}
</span>
</div>
</div>
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
<span>Jan 2026</span>
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
<span>8 min read</span>
</div>
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{post.h1}
</h2>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-8">
{post.description}
</p>
<div className="mt-auto">
<span className="inline-flex items-center gap-2 font-bold text-blue-600 dark:text-white group-hover:gap-3 transition-all">
Read article <span className="material-symbols-outlined text-sm">arrow_forward</span>
</span>
</div>
</div>
</motion.div>
</Link>
))}
</div>
</section>
<Contact />
</div>
);
};
export default BlogPage;

322
src/pages/BlogPostPage.tsx Normal file
View File

@ -0,0 +1,322 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import { Link } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SEO from '../../components/SEO';
import Services from '../../components/Services';
import CTA from '../../components/CTA';
import AreasWeServe from '../../components/AreasWeServe';
import { BlogPostData } from '../data/seoData';
gsap.registerPlugin(ScrollTrigger);
interface BlogPostPageProps {
data: BlogPostData;
}
const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
const containerRef = useRef<HTMLDivElement>(null);
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// Parallax Background
if (parallaxWrapperRef.current) {
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();
}, []);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const category = data.slug.includes('corpus-christi-blog') ||
data.slug.includes('portland-tx') ||
data.slug.includes('rockport-tx') ||
data.slug.includes('aransas-pass') ||
data.slug.includes('kingsville-tx')
? 'Local Services'
: 'IT Insights';
return (
<>
<SEO
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
/>
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
{/* Hero Section */}
<section
ref={containerRef}
onMouseMove={handleMouseMove}
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
>
{/* Parallax Background */}
<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 */}
<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"
/>
{/* Highlight Layer */}
<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"
/>
</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>
{/* Hero Content */}
<div className="relative z-10 text-center max-w-4xl px-6">
{/* Breadcrumbs */}
<nav className="hero-stagger mb-8 text-sm">
<ol className="flex items-center gap-2 text-gray-600 dark:text-gray-400 justify-center">
<li>
<Link to="/" className="hover:text-gray-900 dark:hover:text-white transition-colors">
Home
</Link>
</li>
<span className="material-symbols-outlined text-xs">chevron_right</span>
<li>
<Link to="/blog" className="hover:text-gray-900 dark:hover:text-white transition-colors">
Blog
</Link>
</li>
<span className="material-symbols-outlined text-xs">chevron_right</span>
<li className="text-gray-900 dark:text-white font-medium">{category}</li>
</ol>
</nav>
<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">
{category}
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
</div>
<h1 className="hero-stagger font-display text-4xl md:text-6xl lg:text-7xl font-medium tracking-tighter leading-[1.1] mb-8 text-gray-900 dark:text-white">
{data.h1}
</h1>
{/* Meta Info */}
<div className="hero-stagger flex items-center gap-6 text-gray-600 dark:text-gray-400 mb-8 justify-center">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-sm">schedule</span>
<span>5 min read</span>
</div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-sm">calendar_today</span>
<span>January 2025</span>
</div>
</div>
{/* Featured Image */}
{data.image && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4, duration: 0.8 }}
className="hero-stagger rounded-2xl overflow-hidden border border-gray-200 dark:border-white/10 shadow-2xl mb-8 max-w-md mx-auto"
>
<img
src={data.image}
alt={data.h1}
className="w-full h-auto max-h-64 object-cover"
/>
</motion.div>
)}
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
<motion.a
href="/contact"
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
Get IT Support
</motion.a>
<motion.a
href="/it-support-corpus-christi"
className="px-8 py-3 bg-white/10 dark:bg-white/10 backdrop-blur-sm border-2 border-white/40 dark:border-white/40 text-white dark:text-white rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
View All Services
</motion.a>
</div>
</div>
</section>
{/* Main Content Section */}
<section className="px-6 py-16 relative">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white/80 dark:bg-white/5 backdrop-blur-xl rounded-3xl p-12 md:p-16 shadow-2xl border border-gray-100 dark:border-white/10"
>
<div className="prose prose-lg md:prose-xl dark:prose-invert max-w-none prose-headings:font-display prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4 prose-p:leading-relaxed prose-li:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</motion.div>
</div>
</section>
{/* CTA Section */}
<section className="px-6 py-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<div className="p-12 bg-gradient-to-br from-blue-50 to-gray-50 dark:from-blue-950/30 dark:to-gray-950/30 rounded-3xl border border-blue-100 dark:border-blue-900/50 shadow-xl text-center">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-6 block">
{category === 'Local Services' ? 'location_on' : 'insights'}
</span>
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">
{category === 'Local Services'
? 'Ready to Get IT Support in Your Area?'
: 'Need Expert IT Support for Your Business?'}
</h2>
<p className="text-lg text-gray-700 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
{category === 'Local Services'
? 'Contact us today to learn how we can help your business with reliable IT support and managed services.'
: 'Let us handle your IT needs so you can focus on growing your business. Get a free consultation today.'}
</p>
<Link
to="/contact"
className="inline-flex items-center gap-3 px-10 py-5 bg-black dark:bg-white text-white dark:text-black rounded-full font-bold text-lg transition-all hover:scale-105 shadow-2xl hover:shadow-3xl"
>
Get Started
<span className="material-symbols-outlined">arrow_forward</span>
</Link>
</div>
</motion.div>
</section>
{/* Related Content Grid */}
<section className="px-6 py-16">
<div className="max-w-6xl mx-auto">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="font-display text-4xl font-bold mb-12 text-center text-gray-900 dark:text-white"
>
Why Choose Bay Area IT?
</motion.h2>
<div className="grid md:grid-cols-3 gap-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
verified_user
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Proven Expertise
</h3>
<p className="text-gray-600 dark:text-gray-300">
Years of experience serving businesses across the Coastal Bend with comprehensive IT solutions.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
support_agent
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
24/7 Support
</h3>
<p className="text-gray-600 dark:text-gray-300">
Round-the-clock monitoring and support to keep your business running smoothly at all times.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
handshake
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Local Partnership
</h3>
<p className="text-gray-600 dark:text-gray-300">
A trusted local partner who understands your community and business needs.
</p>
</motion.div>
</div>
</div>
</section>
{/* Services Section */}
<Services preview={true} />
{/* Areas We Serve & CTA */}
<AreasWeServe />
<CTA />
</div>
</>
);
};
export default BlogPostPage;

View File

@ -1,207 +1,207 @@
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
const ContactPage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const faqs = [
{ q: 'How quickly can you start?', a: 'Most assessments can begin within 48 hours of contact. Emergency support is available 24/7.' },
{ q: 'How do you price services?', a: 'Transparent monthly pricing based on devices and services needed. No hidden fees or surprise charges.' },
{ q: 'What\'s included in support?', a: '24/7 monitoring, helpdesk, proactive maintenance, security updates, and SLA guarantees.' },
];
return (
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
{/* Hero */}
<section className="py-20 px-6 text-center border-b border-white/5 relative z-10">
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
Let's talk about <br /><span className="text-gray-500">your IT needs</span>
</h1>
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
Ready to improve your technology? We're here to help. Get started with a free consultation and see how we can make your IT work better for you.
</p>
</section>
<section className="py-24 px-6 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 grid lg:grid-cols-2 gap-16">
{/* Left: Contact Form */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-white/5 backdrop-blur-sm p-8 md:p-10 rounded-3xl shadow-xl border border-white/10"
>
<h3 className="text-2xl font-bold mb-8 text-white">Send us a message</h3>
<form 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-300 mb-2">Name *</label>
<input type="text" id="name" required className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="John Doe" />
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-300 mb-2">Phone</label>
<input type="tel" id="phone" className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="(555) 000-0000" />
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">Email *</label>
<input type="email" id="email" required className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="john@company.com" />
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-300 mb-2">Company</label>
<input type="text" id="company" className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="Acme Inc." />
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-300 mb-2">Message *</label>
<textarea id="message" rows={4} required className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600 resize-none" placeholder="How can we help you?"></textarea>
</div>
<button type="submit" className="w-full py-4 bg-white text-black font-bold rounded-xl transition-all hover:bg-gray-200 hover:scale-[1.02] shadow-[0_0_20px_rgba(255,255,255,0.1)]">
Send Message
</button>
</form>
</motion.div>
{/* Right: FAQ & Info */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="space-y-12"
>
{/* Contact Info */}
<div className="grid sm:grid-cols-2 gap-8">
<div className="bg-white/5 backdrop-blur-sm p-6 rounded-2xl border border-white/10 hover:border-white/20 transition-colors">
<div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center text-white mb-4">
<span className="material-symbols-outlined">call</span>
</div>
<h4 className="font-bold text-white mb-1">Phone</h4>
<p className="text-gray-400">(361) 765-8400</p>
</div>
<div className="bg-white/5 backdrop-blur-sm p-6 rounded-2xl border border-white/10 hover:border-white/20 transition-colors">
<div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center text-white mb-4">
<span className="material-symbols-outlined">location_on</span>
</div>
<h4 className="font-bold text-white mb-1">Address</h4>
<p className="text-gray-400">1001 Blucher St,<br />Corpus Christi, TX 78401</p>
</div>
</div>
<div className="bg-white/5 backdrop-blur-sm p-8 rounded-2xl border border-white/10">
<h4 className="font-bold text-white mb-4">Hours & Area</h4>
<p className="text-gray-400 mb-2"><span className="font-semibold text-white">Mon - Fri:</span> 8:00 AM - 6:00 PM</p>
<p className="text-gray-500 text-sm mb-4">(Emergency support: 24/7)</p>
<p className="text-gray-400"><span className="font-semibold text-white">Service Area:</span> Corpus Christi & the Coastal Bend (including Portland, Rockport, Aransas Pass, Kingsville, Port Aransas)</p>
</div>
{/* FAQ */}
<div>
<h3 className="text-2xl font-bold mb-6 text-white">Quick Answers</h3>
<div className="space-y-4">
{faqs.map((faq, index) => (
<div key={index} className="bg-white/5 backdrop-blur-sm p-6 rounded-2xl border border-white/10 hover:border-white/30 transition-colors">
<h4 className="font-bold text-white mb-2">{faq.q}</h4>
<p className="text-gray-400 text-sm leading-relaxed">{faq.a}</p>
</div>
))}
</div>
</div>
</motion.div>
</div>
</section>
{/* What Happens Next Section */}
<section className="py-24 px-6 border-t border-white/5">
<div className="max-w-7xl mx-auto">
<h2 className="font-display text-3xl md:text-4xl font-bold text-center mb-16 text-white">
What happens next?
</h2>
<div className="grid md:grid-cols-3 gap-8">
{[
{
step: "01",
title: "We respond quickly",
description: "Get a response within 24 hours, usually much faster."
},
{
step: "02",
title: "Free consultation",
description: "20-minute call to understand your needs and challenges."
},
{
step: "03",
title: "Custom proposal",
description: "Tailored solution with clear next steps and pricing."
}
].map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="bg-white/5 backdrop-blur-sm p-8 rounded-2xl border border-white/10 hover:bg-white/10 transition-colors relative overflow-hidden group"
>
<div className="absolute top-0 right-0 p-8 opacity-10 font-display text-8xl font-bold text-white group-hover:scale-110 transition-transform select-none pointer-events-none">
{item.step}
</div>
<div className="relative z-10">
<div className="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center text-white mb-6 border border-white/10">
<span className="material-symbols-outlined">
{index === 0 ? 'schedule_send' : index === 1 ? 'phone_in_talk' : 'contract_edit'}
</span>
</div>
<h3 className="text-xl font-bold text-white mb-3">{item.title}</h3>
<p className="text-gray-400 leading-relaxed">{item.description}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Service Area Section */}
<section className="py-24 px-6 relative overflow-hidden">
<div className="absolute inset-0 bg-white/5"></div>
<div className="max-w-7xl mx-auto relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
<div>
<h2 className="font-display text-3xl md:text-4xl font-bold mb-6 text-white">
Our Service Area
</h2>
<p className="text-lg text-gray-400 mb-8 leading-relaxed">
Proudly serving Corpus Christi, Portland, Rockport, Aransas Pass, Kingsville, Port Aransas, and the entire Coastal Bend region.
</p>
<div className="flex flex-wrap gap-3">
{['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville', 'Port Aransas'].map((city) => (
<div key={city} className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 text-gray-300">
<span className="material-symbols-outlined text-sm text-white">location_on</span>
{city}
</div>
))}
</div>
</div>
<div className="h-[400px] bg-[#1a1a1a] rounded-3xl overflow-hidden border border-white/10 shadow-2xl relative">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3532.1843778537446!2d-97.39864222453538!3d27.79426697613324!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x86685f8090787b65%3A0x6762397985799732!2s1001%20Blucher%20St%2C%20Corpus%20Christi%2C%20TX%2078401!5e0!3m2!1sen!2sus!4v1709420000000!5m2!1sen!2sus"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
></iframe>
</div>
</div>
</div>
</section>
</div>
);
};
export default ContactPage;
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
const ContactPage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const faqs = [
{ q: 'How quickly can you start?', a: 'Most assessments can begin within 48 hours of contact. Emergency support is available 24/7.' },
{ q: 'How do you price services?', a: 'Transparent monthly pricing based on devices and services needed. No hidden fees or surprise charges.' },
{ q: 'What\'s included in support?', a: '24/7 monitoring, helpdesk, proactive maintenance, security updates, and SLA guarantees.' },
];
return (
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
{/* Hero */}
<section className="py-20 px-6 text-center border-b border-white/5 relative z-10">
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
Let's talk about <br /><span className="text-gray-500">your IT needs</span>
</h1>
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
Ready to improve your technology? We're here to help. Get started with a free consultation and see how we can make your IT work better for you.
</p>
</section>
<section className="py-24 px-6 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 grid lg:grid-cols-2 gap-16">
{/* Left: Contact Form */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-white/5 backdrop-blur-sm p-8 md:p-10 rounded-3xl shadow-xl border border-white/10"
>
<h3 className="text-2xl font-bold mb-8 text-white">Send us a message</h3>
<form 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-300 mb-2">Name *</label>
<input type="text" id="name" required className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="John Doe" />
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-300 mb-2">Phone</label>
<input type="tel" id="phone" className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="(555) 000-0000" />
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">Email *</label>
<input type="email" id="email" required className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="john@company.com" />
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-300 mb-2">Company</label>
<input type="text" id="company" className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600" placeholder="Acme Inc." />
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-300 mb-2">Message *</label>
<textarea id="message" rows={4} required className="w-full px-4 py-3 rounded-xl bg-black/20 border border-white/10 focus:border-white/30 focus:ring-1 focus:ring-white/30 outline-none transition-all text-white placeholder-gray-600 resize-none" placeholder="How can we help you?"></textarea>
</div>
<button type="submit" className="w-full py-4 bg-white text-black font-bold rounded-xl transition-all hover:bg-gray-200 hover:scale-[1.02] shadow-[0_0_20px_rgba(255,255,255,0.1)]">
Send Message
</button>
</form>
</motion.div>
{/* Right: FAQ & Info */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="space-y-12"
>
{/* Contact Info */}
<div className="grid sm:grid-cols-2 gap-8">
<div className="bg-white/5 backdrop-blur-sm p-6 rounded-2xl border border-white/10 hover:border-white/20 transition-colors">
<div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center text-white mb-4">
<span className="material-symbols-outlined">call</span>
</div>
<h4 className="font-bold text-white mb-1">Phone</h4>
<p className="text-gray-400">(361) 765-8400</p>
</div>
<div className="bg-white/5 backdrop-blur-sm p-6 rounded-2xl border border-white/10 hover:border-white/20 transition-colors">
<div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center text-white mb-4">
<span className="material-symbols-outlined">location_on</span>
</div>
<h4 className="font-bold text-white mb-1">Address</h4>
<p className="text-gray-400">1001 Blucher St,<br />Corpus Christi, TX 78401</p>
</div>
</div>
<div className="bg-white/5 backdrop-blur-sm p-8 rounded-2xl border border-white/10">
<h4 className="font-bold text-white mb-4">Hours & Area</h4>
<p className="text-gray-400 mb-2"><span className="font-semibold text-white">Mon - Fri:</span> 8:00 AM - 6:00 PM</p>
<p className="text-gray-500 text-sm mb-4">(Emergency support: 24/7)</p>
<p className="text-gray-400"><span className="font-semibold text-white">Service Area:</span> Corpus Christi & the Coastal Bend (including Portland, Rockport, Aransas Pass, Kingsville, Port Aransas)</p>
</div>
{/* FAQ */}
<div>
<h3 className="text-2xl font-bold mb-6 text-white">Quick Answers</h3>
<div className="space-y-4">
{faqs.map((faq, index) => (
<div key={index} className="bg-white/5 backdrop-blur-sm p-6 rounded-2xl border border-white/10 hover:border-white/30 transition-colors">
<h4 className="font-bold text-white mb-2">{faq.q}</h4>
<p className="text-gray-400 text-sm leading-relaxed">{faq.a}</p>
</div>
))}
</div>
</div>
</motion.div>
</div>
</section>
{/* What Happens Next Section */}
<section className="py-24 px-6 border-t border-white/5">
<div className="max-w-7xl mx-auto">
<h2 className="font-display text-3xl md:text-4xl font-bold text-center mb-16 text-white">
What happens next?
</h2>
<div className="grid md:grid-cols-3 gap-8">
{[
{
step: "01",
title: "We respond quickly",
description: "Get a response within 24 hours, usually much faster."
},
{
step: "02",
title: "Free consultation",
description: "20-minute call to understand your needs and challenges."
},
{
step: "03",
title: "Custom proposal",
description: "Tailored solution with clear next steps and pricing."
}
].map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="bg-white/5 backdrop-blur-sm p-8 rounded-2xl border border-white/10 hover:bg-white/10 transition-colors relative overflow-hidden group"
>
<div className="absolute top-0 right-0 p-8 opacity-10 font-display text-8xl font-bold text-white group-hover:scale-110 transition-transform select-none pointer-events-none">
{item.step}
</div>
<div className="relative z-10">
<div className="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center text-white mb-6 border border-white/10">
<span className="material-symbols-outlined">
{index === 0 ? 'schedule_send' : index === 1 ? 'phone_in_talk' : 'contract_edit'}
</span>
</div>
<h3 className="text-xl font-bold text-white mb-3">{item.title}</h3>
<p className="text-gray-400 leading-relaxed">{item.description}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Service Area Section */}
<section className="py-24 px-6 relative overflow-hidden">
<div className="absolute inset-0 bg-white/5"></div>
<div className="max-w-7xl mx-auto relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
<div>
<h2 className="font-display text-3xl md:text-4xl font-bold mb-6 text-white">
Our Service Area
</h2>
<p className="text-lg text-gray-400 mb-8 leading-relaxed">
Proudly serving Corpus Christi, Portland, Rockport, Aransas Pass, Kingsville, Port Aransas, and the entire Coastal Bend region.
</p>
<div className="flex flex-wrap gap-3">
{['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville', 'Port Aransas'].map((city) => (
<div key={city} className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 text-gray-300">
<span className="material-symbols-outlined text-sm text-white">location_on</span>
{city}
</div>
))}
</div>
</div>
<div className="h-[400px] bg-[#1a1a1a] rounded-3xl overflow-hidden border border-white/10 shadow-2xl relative">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3532.1843778537446!2d-97.39864222453538!3d27.79426697613324!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x86685f8090787b65%3A0x6762397985799732!2s1001%20Blucher%20St%2C%20Corpus%20Christi%2C%20TX%2078401!5e0!3m2!1sen!2sus!4v1709420000000!5m2!1sen!2sus"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
></iframe>
</div>
</div>
</div>
</section>
</div>
);
};
export default ContactPage;

View File

@ -1,28 +1,92 @@
import React, { useEffect } from 'react';
import Hero from '../../components/Hero';
import Mission from '../../components/Mission';
import Services from '../../components/Services';
import Process from '../../components/Process';
import Blog from '../../components/Blog';
import Testimonials from '../../components/Testimonials';
import CTA from '../../components/CTA';
const HomePage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<>
<Hero />
<Mission />
<Services preview={true} />
<Process />
<Blog />
<Testimonials />
<CTA />
</>
);
};
export default HomePage;
import React, { useEffect } from 'react';
import Hero from '../../components/Hero';
import Mission from '../../components/Mission';
import Services from '../../components/Services';
import Process from '../../components/Process';
import Blog from '../../components/Blog';
import Testimonials from '../../components/Testimonials';
import CTA from '../../components/CTA';
import SEO from '../../components/SEO';
import FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { locationData } from '../data/seoData';
const HomePage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
// Enhanced LocalBusiness Schema per SEO plan
const schema = {
"@context": "https://schema.org",
"@type": "ITService",
"name": "Bay Area IT Services",
"image": "https://bayarea-cc.com/logo.png",
"@id": "https://bayarea-cc.com",
"url": "https://bayarea-cc.com",
"telephone": "+1-361-XXX-XXXX", // TODO: Replace with actual phone
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "[YOUR STREET]", // TODO: Add actual address
"addressLocality": "Corpus Christi",
"addressRegion": "TX",
"postalCode": "[YOUR ZIP]", // TODO: Add actual ZIP
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 27.800583,
"longitude": -97.39638
},
"areaServed": [
{ "@type": "City", "name": "Corpus Christi" },
{ "@type": "City", "name": "Portland" },
{ "@type": "City", "name": "Rockport" },
{ "@type": "City", "name": "Aransas Pass" },
{ "@type": "City", "name": "Kingsville" }
],
"serviceType": [
"IT Support",
"Business IT Support",
"Outsourced IT Services",
"Computer Network Support",
"Cyber Security"
],
"openingHoursSpecification": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday"
],
"opens": "08:00",
"closes": "18:00"
}
};
return (
<>
<SEO
title="IT Service & IT Support for Businesses in Corpus Christi, TX"
description="Reliable IT support and IT services for businesses in Corpus Christi, TX. Fast response, outsourced IT support & help desk solutions. Call now."
keywords={["IT Service", "IT Support", "Corpus Christi", "Business IT Support"]}
canonicalUrl={window.location.href}
schema={schema}
/>
<Hero />
<Mission />
<Services preview={true} />
<Process />
<Blog />
<Testimonials />
<AreasWeServe />
<FAQ items={locationData[0].faq} />
<CTA />
</>
);
};
export default HomePage;

214
src/pages/LocationPage.tsx Normal file
View File

@ -0,0 +1,214 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import { Link } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SEO from '../../components/SEO';
import Services from '../../components/Services';
import CTA from '../../components/CTA';
import FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { LocationData } from '../data/seoData';
gsap.registerPlugin(ScrollTrigger);
interface LocationPageProps {
data: LocationData;
}
const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
const containerRef = useRef<HTMLDivElement>(null);
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// Parallax Background
if (parallaxWrapperRef.current) {
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();
}, []);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const schema = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Bay Area IT Services",
"url": window.location.href,
"areaServed": {
"@type": "City",
"name": data.city
}
};
return (
<>
<SEO
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
schema={schema}
/>
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
{/* Hero Section */}
<section
ref={containerRef}
onMouseMove={handleMouseMove}
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
>
{/* Parallax Background */}
<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 */}
<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"
/>
{/* Highlight Layer */}
<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"
/>
</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>
{/* Hero Content */}
<div className="relative z-10 text-center max-w-4xl px-6">
<div className="hero-stagger flex items-center justify-center gap-2 mb-4">
<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">
{data.city}, Texas IT Support
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
</div>
<h1 className="hero-stagger font-display text-4xl md:text-5xl lg:text-6xl font-medium tracking-tighter leading-[1.1] mb-5 text-gray-900 dark:text-white">
{data.h1.split(' ').slice(0, -2).join(' ')}<br />
<span className="text-gray-500 dark:text-gray-500">
{data.h1.split(' ').slice(-2).join(' ')}
</span>
</h1>
<p className="hero-stagger text-base md:text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-6 font-light leading-relaxed">
{data.description}
</p>
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
<motion.a
href="/contact"
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
Get IT Support
</motion.a>
<motion.a
href="/it-support-corpus-christi"
className="px-8 py-3 bg-white/10 dark:bg-white/10 backdrop-blur-sm border-2 border-white/40 dark:border-white/40 text-white dark:text-white rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
View All Services
</motion.a>
</div>
</div>
</section>
{/* Main Content Section */}
<section className="px-6 py-16 relative">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white/80 dark:bg-white/5 backdrop-blur-xl rounded-3xl p-12 md:p-16 shadow-2xl border border-gray-100 dark:border-white/10"
>
<div className="prose prose-lg md:prose-xl dark:prose-invert max-w-none prose-headings:font-display prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h3:text-2xl prose-p:leading-relaxed prose-li:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</motion.div>
</div>
</section>
{/* Internal Linking Card (if not Corpus Christi) */}
{data.city !== "Corpus Christi" && (
<section className="px-6 py-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<div className="p-10 bg-gradient-to-br from-blue-50 to-gray-50 dark:from-blue-950/30 dark:to-gray-950/30 rounded-3xl border border-blue-100 dark:border-blue-900/50 shadow-xl">
<div className="flex items-start gap-4">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-4xl">
info
</span>
<div>
<h3 className="font-display text-2xl font-bold mb-3 text-gray-900 dark:text-white">
Serving the Coastal Bend
</h3>
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
We are based in Corpus Christi and proudly serve businesses throughout the entire surrounding area.
Learn more about our <Link to="/it-support-corpus-christi" className="text-blue-600 dark:text-blue-400 font-bold hover:underline">IT support in Corpus Christi</Link> and our commitment to local businesses.
</p>
</div>
</div>
</div>
</motion.div>
</section>
)}
{/* Services Section */}
<Services featuredIds={[10, 9, 11]} />
{/* Areas We Serve & CTA */}
<AreasWeServe />
<FAQ items={data.faq} />
<CTA />
</div>
</>
);
};
export default LocationPage;

232
src/pages/ServicePage.tsx Normal file
View File

@ -0,0 +1,232 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import { Link } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SEO from '../../components/SEO';
import Services from '../../components/Services';
import CTA from '../../components/CTA';
import FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { ServiceData } from '../data/seoData';
gsap.registerPlugin(ScrollTrigger);
interface ServicePageProps {
data: ServiceData;
}
const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
const containerRef = useRef<HTMLDivElement>(null);
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// Parallax Background
if (parallaxWrapperRef.current) {
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();
}, []);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<>
<SEO
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
/>
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
{/* Hero Section */}
<section
ref={containerRef}
onMouseMove={handleMouseMove}
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
>
{/* Parallax Background */}
<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 */}
<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"
/>
{/* Highlight Layer */}
<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"
/>
</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>
{/* Hero Content */}
<div className="relative z-10 text-center max-w-4xl px-6">
<div className="hero-stagger flex items-center justify-center gap-2 mb-4">
<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">
Professional IT Services
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
</div>
<h1 className="hero-stagger font-display text-4xl md:text-5xl lg:text-6xl font-medium tracking-tighter leading-[1.1] mb-5 text-gray-900 dark:text-white">
{data.h1.split(' ').slice(0, -2).join(' ')}<br />
<span className="text-gray-500 dark:text-gray-500">
{data.h1.split(' ').slice(-2).join(' ')}
</span>
</h1>
<p className="hero-stagger text-base md:text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-6 font-light leading-relaxed">
{data.description}
</p>
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
<motion.a
href="/contact"
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
Get Started
</motion.a>
<motion.a
href="/it-support-corpus-christi"
className="px-8 py-3 bg-white/10 dark:bg-white/10 backdrop-blur-sm border-2 border-white/40 dark:border-white/40 text-white dark:text-white rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
View All Services
</motion.a>
</div>
</div>
</section>
{/* Main Content Section */}
<section className="px-6 py-16 relative">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white/80 dark:bg-white/5 backdrop-blur-xl rounded-3xl p-12 md:p-16 shadow-2xl border border-gray-100 dark:border-white/10"
>
<div className="prose prose-lg md:prose-xl dark:prose-invert max-w-none prose-headings:font-display prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h3:text-2xl prose-p:leading-relaxed prose-li:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</motion.div>
</div>
</section>
{/* Feature Highlight Section */}
<section className="px-6 py-16">
<div className="max-w-6xl mx-auto grid md:grid-cols-3 gap-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
speed
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Fast Response
</h3>
<p className="text-gray-600 dark:text-gray-300">
Quick resolution of IT issues to minimize downtime and keep your business running smoothly.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
verified_user
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Proactive Security
</h3>
<p className="text-gray-600 dark:text-gray-300">
Advanced security measures and monitoring to protect your business from cyber threats.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
support_agent
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Expert Team
</h3>
<p className="text-gray-600 dark:text-gray-300">
Experienced IT professionals dedicated to providing exceptional service and support.
</p>
</motion.div>
</div>
</section>
{/* Services Section */}
<Services featuredIds={data.relatedServices} />
{/* Areas We Serve & CTA */}
<AreasWeServe />
<FAQ items={data.faq} />
<CTA />
</div>
</>
);
};
export default ServicePage;

View File

@ -1,414 +1,415 @@
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence, useScroll, useTransform, useMotionValueEvent } from 'framer-motion';
import Contact from '../../components/Contact';
const services = [
{
id: 1,
title: 'Windows 11 Transition',
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
challenge: 'Running outdated operating systems leaves your business vulnerable to security threats and compatibility issues.',
approach: 'We manage the entire migration process, from hardware compatibility checks to software deployment and user training.',
deliverables: [
'Hardware compatibility assessment',
'Windows 11 deployment and configuration',
'Application compatibility testing',
'Security policy implementation',
'User training sessions'
],
needs: [
'Current device inventory',
'Software list',
'User schedule for upgrades'
],
icon: 'desktop_windows',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
},
{
id: 2,
title: 'Web Services',
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
challenge: 'A poor online presence can cost you customers and credibility in a digital-first world.',
approach: 'We build professional, responsive websites and manage your digital identity to attract and retain customers.',
deliverables: [
'Custom website design & development',
'Domain registration & management',
'Professional email setup (M365/Google)',
'SEO optimization basics',
'Hosting & maintenance'
],
needs: [
'Brand guidelines / Logo',
'Content & copy',
'Domain access (if existing)'
],
icon: 'language',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
},
{
id: 3,
title: 'Performance Upgrades',
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
challenge: 'Slow computers kill productivity and frustrate employees, leading to wasted time.',
approach: 'We breathe new life into existing hardware with cost-effective upgrades and optimizations.',
deliverables: [
'SSD installation & cloning',
'RAM upgrades',
'System cleanup & optimization',
'Thermal paste replacement',
'Benchmark reporting'
],
needs: [
'Access to devices',
'Data backup confirmation'
],
icon: 'speed',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
},
{
id: 4,
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
challenge: 'Printer connectivity issues are a leading cause of office support tickets and downtime.',
approach: 'We set up reliable printing environments with proper drivers, networking, and user access controls.',
deliverables: [
'Network printer setup',
'Scanner configuration (Scan-to-Email/Folder)',
'Print server management',
'One-click user deployment',
'Troubleshooting training'
],
needs: [
'Printer/Scanner hardware',
'Network access details'
],
icon: 'print',
image: ''
},
{
id: 5,
title: 'New/Refurbished Desktop Hardware',
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
challenge: 'Sourcing the right hardware at the right price can be time-consuming and risky.',
approach: 'We source high-quality new and refurbished equipment that meets your specs and budget, fully tested and ready to go.',
deliverables: [
'Hardware procurement',
'Quality assurance testing',
'Image deployment',
'Peripherals setup',
'Warranty management'
],
needs: [
'Budget constraints',
'Performance requirements'
],
icon: 'computer',
image: ''
},
{
id: 6,
title: 'VPN Setup',
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
challenge: 'Remote work requires secure access to internal resources without exposing your network to threats.',
approach: 'We implement robust VPN solutions like WireGuard or OpenVPN for secure, encrypted remote connectivity.',
deliverables: [
'VPN server configuration',
'Client software deployment',
'Access control lists',
'Connection testing',
'User guides'
],
needs: [
'Public IP / DNS details',
'User list'
],
icon: 'vpn_lock',
image: ''
},
{
id: 7,
title: 'Network Infrastructure Support',
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
challenge: 'A weak network backbone leads to slow speeds, dropped calls, and security holes.',
approach: 'We design and maintain enterprise-grade networks that handle your data traffic reliably and securely.',
deliverables: [
'Router & Switch configuration',
'VLAN segmentation',
'Wi-Fi optimization',
'Network monitoring setup',
'Cable management'
],
needs: [
'Floor plans (for Wi-Fi)',
'ISP details'
],
icon: 'lan',
image: ''
},
{
id: 8,
title: 'Network Attached Storage',
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
challenge: 'Data growth requires scalable storage that is accessible yet secure from loss.',
approach: 'We deploy NAS solutions that centralize your data with redundancy and easy access for your team.',
deliverables: [
'NAS hardware selection & setup',
'RAID configuration',
'User permission management',
'Remote access configuration',
'Backup integration'
],
needs: [
'Capacity requirements',
'Access patterns'
],
icon: 'storage',
image: ''
}
];
const ServiceModal: React.FC<{ service: typeof services[0] | null; onClose: () => void }> = ({ service, onClose }) => {
if (!service) return null;
// ESC key & Body Scroll Lock
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
// Lock scroll on both html and body to prevent background scrolling
const originalHtmlOverflow = document.documentElement.style.overflow;
const originalBodyOverflow = document.body.style.overflow;
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleEsc);
return () => {
window.removeEventListener('keydown', handleEsc);
document.documentElement.style.overflow = originalHtmlOverflow;
document.body.style.overflow = originalBodyOverflow;
};
}, [onClose]);
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-[#1a1a1a] border border-white/10 rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto relative shadow-2xl custom-scrollbar"
onClick={(e) => e.stopPropagation()}
>
{/* Close Button - Sticky and distinct */}
<button
onClick={onClose}
className="fixed md:absolute top-6 right-6 p-2 text-white bg-black/60 hover:bg-white/20 backdrop-blur-sm rounded-full transition-all z-50 group border border-white/10 hover:border-white/40 shadow-lg"
aria-label="Close modal"
>
<span className="material-symbols-outlined group-hover:scale-110 transition-transform">close</span>
</button>
{/* Hero Image in Modal */}
{service.image && (
<div className="w-full h-64 relative">
<img
src={service.image}
alt={service.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#1a1a1a] to-transparent"></div>
</div>
)}
<div className="p-8 md:p-12 relative">
<div className="flex items-center gap-6 mb-8">
<div className="w-16 h-16 bg-white/5 border border-white/10 rounded-2xl flex items-center justify-center shrink-0">
<span className="material-symbols-outlined text-3xl text-white">{service.icon}</span>
</div>
<div>
<h2 className="text-3xl font-bold text-white mb-2">{service.title}</h2>
<p className="text-gray-400 text-lg">{service.description}</p>
</div>
</div>
{/* ... rest of content */}
<div className="grid md:grid-cols-2 gap-12 mb-12">
<div>
<h4 className="text-white font-bold uppercase tracking-wider text-xs mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-sm">warning</span> The Challenge
</h4>
<p className="text-gray-300 leading-relaxed text-sm md:text-base">{service.challenge}</p>
</div>
<div>
<h4 className="text-white font-bold uppercase tracking-wider text-xs mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-sm">lightbulb</span> Our Approach
</h4>
<p className="text-gray-300 leading-relaxed text-sm md:text-base">{service.approach}</p>
</div>
</div>
<div className="mb-8">
<h4 className="font-bold text-white mb-6 flex items-center gap-2">
<span className="material-symbols-outlined">check_circle</span>
What We Deliver
</h4>
<div className="grid md:grid-cols-2 gap-4">
{service.deliverables.map((item, i) => (
<div key={i} className="flex items-start gap-3 p-3 rounded-xl bg-white/5 border border-white/5 hover:border-white/20 transition-colors">
<span className="w-1.5 h-1.5 rounded-full bg-white mt-2 flex-shrink-0 shadow-[0_0_8px_rgba(255,255,255,0.5)]"></span>
<span className="text-gray-300 text-sm">{item}</span>
</div>
))}
</div>
</div>
<div className="pt-8 border-t border-white/10">
<h4 className="font-semibold text-white mb-3 text-sm uppercase tracking-wide opacity-80">What we need from you</h4>
<ul className="text-sm text-gray-400 space-y-2">
{service.needs.map((item, i) => (
<li key={i} className="flex items-center gap-2">
<span className="w-1 h-1 rounded-full bg-white/50"></span>
{item}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</motion.div>
);
};
const ServicesPage: React.FC = () => {
const [selectedService, setSelectedService] = useState<typeof services[0] | null>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end center"]
});
const heightTransform = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
const [activeTimelineIndex, setActiveTimelineIndex] = useState(0);
useMotionValueEvent(scrollYProgress, "change", (latest) => {
// Calculate index based on scroll progress (0-1)
// Adjust multiplier to trigger slightly earlier or exact matches
const index = Math.min(Math.floor((latest) * services.length), services.length - 1);
setActiveTimelineIndex(index);
});
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<div className="pt-20 min-h-screen bg-[#0a0a0a] relative overflow-x-hidden">
{/* Gradient for Services Page */}
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.15),rgba(255,255,255,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
{/* Hero */}
<section className="py-20 px-6 bg-transparent border-b border-white/5 relative z-10">
<div className="max-w-4xl mx-auto text-center">
<span className="text-white/60 font-bold tracking-widest uppercase text-sm mb-3 block">Expertise</span>
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
Complete IT solutions for <br /><span className="text-gray-500">your business</span>
</h1>
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
From desktop support to enterprise infrastructure, we provide the technology foundation your business needs to thrive.
</p>
</div>
</section>
{/* Timeline Section */}
<section ref={containerRef} className="py-24 px-6 relative overflow-hidden 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 relative z-10">
{/* Central Timeline Line */}
<div className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 bottom-0 w-px bg-white/5 -translate-x-1/2 md:translate-x-0"></div>
{/* Active Timeline Line (Scroll Driven) */}
<motion.div
style={{ height: heightTransform }}
className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 w-px bg-white origin-top shadow-[0_0_40px_2px_rgba(255,255,255,1)] drop-shadow-[0_0_10px_rgba(255,255,255,1)] -translate-x-1/2 md:translate-x-0"
></motion.div>
<div className="space-y-64 pb-64">
{services.map((service, index) => (
<div
key={service.id}
className={`relative flex flex-col md:flex-row gap-8 md:gap-0 ${index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'}`}
>
{/* Timeline Dot */}
<motion.div
animate={index <= activeTimelineIndex ? {
scale: 1.2,
opacity: 1,
backgroundColor: "#ffffff",
borderColor: "#ffffff",
boxShadow: "0 0 30px rgba(255,255,255,1)"
} : {
scale: 0.5,
opacity: 0.2,
backgroundColor: "#0a0a0a",
borderColor: "rgba(255,255,255,0.1)",
boxShadow: "none"
}}
viewport={{ margin: "-50% 0px -50% 0px" }}
transition={{ duration: 0.3 }}
className="absolute left-4 md:left-1/2 md:-ml-[6px] -translate-x-1/2 md:translate-x-0 w-3 h-3 rounded-full border border-white/20 z-20 top-1/2 -translate-y-1/2 shrink-0"
></motion.div>
{/* Content Card */}
<motion.div
initial={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
whileInView={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`md:w-1/2 ${index % 2 === 0 ? 'md:pr-24 pl-12' : 'md:pl-24 pl-12'}`}
>
<div
onClick={() => setSelectedService(service)}
className="group cursor-pointer bg-white/5 backdrop-blur-sm rounded-2xl p-10 border border-white/10 hover:border-white/30 hover:bg-white/10 transition-all duration-300 relative overflow-hidden"
>
<div className="w-14 h-14 bg-white/5 rounded-xl flex items-center justify-center mb-8 border border-white/10 group-hover:scale-110 transition-transform duration-500">
<span className="material-symbols-outlined text-3xl text-white">{service.icon}</span>
</div>
<h3 className="text-3xl font-bold text-white mb-4">{service.title}</h3>
<p className="text-gray-400 mb-8 line-clamp-3 text-lg leading-relaxed">{service.description}</p>
<div className="flex items-center text-sm font-bold text-white uppercase tracking-wider group-hover:gap-2 transition-all">
See Details <span className="material-symbols-outlined text-sm ml-1">arrow_forward</span>
</div>
</div>
</motion.div>
{/* Spacer for the other side */}
<div className="md:w-1/2"></div>
</div>
))}
</div>
</div>
</section>
<Contact />
{/* Modal */}
<AnimatePresence>
{selectedService && (
<ServiceModal service={selectedService} onClose={() => setSelectedService(null)} />
)}
</AnimatePresence>
</div>
);
};
export default ServicesPage;
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence, useScroll, useTransform, useMotionValueEvent } from 'framer-motion';
import Contact from '../../components/Contact';
const services = [
{
id: 1,
title: 'Windows 11 Transition',
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
challenge: 'Running outdated operating systems leaves your business vulnerable to security threats and compatibility issues.',
approach: 'We manage the entire migration process, from hardware compatibility checks to software deployment and user training.',
deliverables: [
'Hardware compatibility assessment',
'Windows 11 deployment and configuration',
'Application compatibility testing',
'Security policy implementation',
'User training sessions'
],
needs: [
'Current device inventory',
'Software list',
'User schedule for upgrades'
],
icon: 'desktop_windows',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
},
{
id: 2,
title: 'Web Services',
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
challenge: 'A poor online presence can cost you customers and credibility in a digital-first world.',
approach: 'We build professional, responsive websites and manage your digital identity to attract and retain customers.',
deliverables: [
'Custom website design & development',
'Domain registration & management',
'Professional email setup (M365/Google)',
'SEO optimization basics',
'Hosting & maintenance'
],
needs: [
'Brand guidelines / Logo',
'Content & copy',
'Domain access (if existing)'
],
icon: 'language',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
},
{
id: 3,
title: 'Performance Upgrades',
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
challenge: 'Slow computers kill productivity and frustrate employees, leading to wasted time.',
approach: 'We breathe new life into existing hardware with cost-effective upgrades and optimizations.',
deliverables: [
'SSD installation & cloning',
'RAM upgrades',
'System cleanup & optimization',
'Thermal paste replacement',
'Benchmark reporting'
],
needs: [
'Access to devices',
'Data backup confirmation'
],
icon: 'speed',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
},
{
id: 4,
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
challenge: 'Printer connectivity issues are a leading cause of office support tickets and downtime.',
approach: 'We set up reliable printing environments with proper drivers, networking, and user access controls.',
deliverables: [
'Network printer setup',
'Scanner configuration (Scan-to-Email/Folder)',
'Print server management',
'One-click user deployment',
'Troubleshooting training'
],
needs: [
'Printer/Scanner hardware',
'Network access details'
],
icon: 'print',
image: ''
},
{
id: 5,
title: 'New/Refurbished Desktop Hardware',
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
challenge: 'Sourcing the right hardware at the right price can be time-consuming and risky.',
approach: 'We source high-quality new and refurbished equipment that meets your specs and budget, fully tested and ready to go.',
deliverables: [
'Hardware procurement',
'Quality assurance testing',
'Image deployment',
'Peripherals setup',
'Warranty management'
],
needs: [
'Budget constraints',
'Performance requirements'
],
icon: 'computer',
image: ''
},
{
id: 6,
title: 'VPN Setup',
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
challenge: 'Remote work requires secure access to internal resources without exposing your network to threats.',
approach: 'We implement robust VPN solutions like WireGuard or OpenVPN for secure, encrypted remote connectivity.',
deliverables: [
'VPN server configuration',
'Client software deployment',
'Access control lists',
'Connection testing',
'User guides'
],
needs: [
'Public IP / DNS details',
'User list'
],
icon: 'vpn_lock',
image: ''
},
{
id: 7,
title: 'Network Infrastructure Support',
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
challenge: 'A weak network backbone leads to slow speeds, dropped calls, and security holes.',
approach: 'We design and maintain enterprise-grade networks that handle your data traffic reliably and securely.',
deliverables: [
'Router & Switch configuration',
'VLAN segmentation',
'Wi-Fi optimization',
'Network monitoring setup',
'Cable management'
],
needs: [
'Floor plans (for Wi-Fi)',
'ISP details'
],
icon: 'lan',
image: ''
},
{
id: 8,
title: 'Network Attached Storage',
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
challenge: 'Data growth requires scalable storage that is accessible yet secure from loss.',
approach: 'We deploy NAS solutions that centralize your data with redundancy and easy access for your team.',
deliverables: [
'NAS hardware selection & setup',
'RAID configuration',
'User permission management',
'Remote access configuration',
'Backup integration'
],
needs: [
'Capacity requirements',
'Access patterns'
],
icon: 'storage',
image: ''
}
];
const ServiceModal: React.FC<{ service: typeof services[0] | null; onClose: () => void }> = ({ service, onClose }) => {
if (!service) return null;
// ESC key & Body Scroll Lock
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
// Lock scroll on both html and body to prevent background scrolling
const originalHtmlOverflow = document.documentElement.style.overflow;
const originalBodyOverflow = document.body.style.overflow;
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleEsc);
return () => {
window.removeEventListener('keydown', handleEsc);
document.documentElement.style.overflow = originalHtmlOverflow;
document.body.style.overflow = originalBodyOverflow;
};
}, [onClose]);
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-[#1a1a1a] border border-white/10 rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto relative shadow-2xl custom-scrollbar"
onClick={(e) => e.stopPropagation()}
data-lenis-prevent
>
{/* Close Button - Sticky and distinct */}
<button
onClick={onClose}
className="fixed md:absolute top-6 right-6 p-2 text-white bg-black/60 hover:bg-white/20 backdrop-blur-sm rounded-full transition-all z-50 group border border-white/10 hover:border-white/40 shadow-lg"
aria-label="Close modal"
>
<span className="material-symbols-outlined group-hover:scale-110 transition-transform">close</span>
</button>
{/* Hero Image in Modal */}
{service.image && (
<div className="w-full h-64 relative">
<img
src={service.image}
alt={service.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#1a1a1a] to-transparent"></div>
</div>
)}
<div className="p-8 md:p-12 relative">
<div className="flex items-center gap-6 mb-8">
<div className="w-16 h-16 bg-white/5 border border-white/10 rounded-2xl flex items-center justify-center shrink-0">
<span className="material-symbols-outlined text-3xl text-white">{service.icon}</span>
</div>
<div>
<h2 className="text-3xl font-bold text-white mb-2">{service.title}</h2>
<p className="text-gray-400 text-lg">{service.description}</p>
</div>
</div>
{/* ... rest of content */}
<div className="grid md:grid-cols-2 gap-12 mb-12">
<div>
<h4 className="text-white font-bold uppercase tracking-wider text-xs mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-sm">warning</span> The Challenge
</h4>
<p className="text-gray-300 leading-relaxed text-sm md:text-base">{service.challenge}</p>
</div>
<div>
<h4 className="text-white font-bold uppercase tracking-wider text-xs mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-sm">lightbulb</span> Our Approach
</h4>
<p className="text-gray-300 leading-relaxed text-sm md:text-base">{service.approach}</p>
</div>
</div>
<div className="mb-8">
<h4 className="font-bold text-white mb-6 flex items-center gap-2">
<span className="material-symbols-outlined">check_circle</span>
What We Deliver
</h4>
<div className="grid md:grid-cols-2 gap-4">
{service.deliverables.map((item, i) => (
<div key={i} className="flex items-start gap-3 p-3 rounded-xl bg-white/5 border border-white/5 hover:border-white/20 transition-colors">
<span className="w-1.5 h-1.5 rounded-full bg-white mt-2 flex-shrink-0 shadow-[0_0_8px_rgba(255,255,255,0.5)]"></span>
<span className="text-gray-300 text-sm">{item}</span>
</div>
))}
</div>
</div>
<div className="pt-8 border-t border-white/10">
<h4 className="font-semibold text-white mb-3 text-sm uppercase tracking-wide opacity-80">What we need from you</h4>
<ul className="text-sm text-gray-400 space-y-2">
{service.needs.map((item, i) => (
<li key={i} className="flex items-center gap-2">
<span className="w-1 h-1 rounded-full bg-white/50"></span>
{item}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</motion.div>
);
};
const ServicesPage: React.FC = () => {
const [selectedService, setSelectedService] = useState<typeof services[0] | null>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end center"]
});
const heightTransform = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
const [activeTimelineIndex, setActiveTimelineIndex] = useState(0);
useMotionValueEvent(scrollYProgress, "change", (latest) => {
// Calculate index based on scroll progress (0-1)
// Adjust multiplier to trigger slightly earlier or exact matches
const index = Math.min(Math.floor((latest) * services.length), services.length - 1);
setActiveTimelineIndex(index);
});
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<div className="pt-20 min-h-screen bg-[#0a0a0a] relative overflow-x-hidden">
{/* Gradient for Services Page */}
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.15),rgba(255,255,255,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
{/* Hero */}
<section className="py-20 px-6 bg-transparent border-b border-white/5 relative z-10">
<div className="max-w-4xl mx-auto text-center">
<span className="text-white/60 font-bold tracking-widest uppercase text-sm mb-3 block">Expertise</span>
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
Complete IT solutions for <br /><span className="text-gray-500">your business</span>
</h1>
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
From desktop support to enterprise infrastructure, we provide the technology foundation your business needs to thrive.
</p>
</div>
</section>
{/* Timeline Section */}
<section ref={containerRef} className="py-24 px-6 relative overflow-hidden 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 relative z-10">
{/* Central Timeline Line */}
<div className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 bottom-0 w-px bg-white/5 -translate-x-1/2 md:translate-x-0"></div>
{/* Active Timeline Line (Scroll Driven) */}
<motion.div
style={{ height: heightTransform }}
className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 w-px bg-white origin-top shadow-[0_0_40px_2px_rgba(255,255,255,1)] drop-shadow-[0_0_10px_rgba(255,255,255,1)] -translate-x-1/2 md:translate-x-0"
></motion.div>
<div className="space-y-64 pb-64">
{services.map((service, index) => (
<div
key={service.id}
className={`relative flex flex-col md:flex-row gap-8 md:gap-0 ${index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'}`}
>
{/* Timeline Dot */}
<motion.div
animate={index <= activeTimelineIndex ? {
scale: 1.2,
opacity: 1,
backgroundColor: "#ffffff",
borderColor: "#ffffff",
boxShadow: "0 0 30px rgba(255,255,255,1)"
} : {
scale: 0.5,
opacity: 0.2,
backgroundColor: "#0a0a0a",
borderColor: "rgba(255,255,255,0.1)",
boxShadow: "none"
}}
viewport={{ margin: "-50% 0px -50% 0px" }}
transition={{ duration: 0.3 }}
className="absolute left-4 md:left-1/2 md:-ml-[6px] -translate-x-1/2 md:translate-x-0 w-3 h-3 rounded-full border border-white/20 z-20 top-1/2 -translate-y-1/2 shrink-0"
></motion.div>
{/* Content Card */}
<motion.div
initial={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
whileInView={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`md:w-1/2 ${index % 2 === 0 ? 'md:pr-24 pl-12' : 'md:pl-24 pl-12'}`}
>
<div
onClick={() => setSelectedService(service)}
className="group cursor-pointer bg-white/5 backdrop-blur-sm rounded-2xl p-10 border border-white/10 hover:border-white/30 hover:bg-white/10 transition-all duration-300 relative overflow-hidden"
>
<div className="w-14 h-14 bg-white/5 rounded-xl flex items-center justify-center mb-8 border border-white/10 group-hover:scale-110 transition-transform duration-500">
<span className="material-symbols-outlined text-3xl text-white">{service.icon}</span>
</div>
<h3 className="text-3xl font-bold text-white mb-4">{service.title}</h3>
<p className="text-gray-400 mb-8 line-clamp-3 text-lg leading-relaxed">{service.description}</p>
<div className="flex items-center text-sm font-bold text-white uppercase tracking-wider group-hover:gap-2 transition-all">
See Details <span className="material-symbols-outlined text-sm ml-1">arrow_forward</span>
</div>
</div>
</motion.div>
{/* Spacer for the other side */}
<div className="md:w-1/2"></div>
</div>
))}
</div>
</div>
</section>
<Contact />
{/* Modal */}
<AnimatePresence>
{selectedService && (
<ServiceModal service={selectedService} onClose={() => setSelectedService(null)} />
)}
</AnimatePresence>
</div>
);
};
export default ServicesPage;