Complete SEO overhaul
31
App.tsx
|
|
@ -12,6 +12,10 @@ import AboutPage from './src/pages/AboutPage';
|
||||||
import ServicesPage from './src/pages/ServicesPage';
|
import ServicesPage from './src/pages/ServicesPage';
|
||||||
import BlogPage from './src/pages/BlogPage';
|
import BlogPage from './src/pages/BlogPage';
|
||||||
import ContactPage from './src/pages/ContactPage';
|
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
|
// Register GSAP plugins globally
|
||||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||||
|
|
@ -69,6 +73,33 @@ const AppContent: React.FC = () => {
|
||||||
<Route path="/services" element={<ServicesPage />} />
|
<Route path="/services" element={<ServicesPage />} />
|
||||||
<Route path="/blog" element={<BlogPage />} />
|
<Route path="/blog" element={<BlogPage />} />
|
||||||
<Route path="/contact" element={<ContactPage />} />
|
<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>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 { motion, AnimatePresence } from 'framer-motion';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
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.',
|
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
|
||||||
icon: 'storage',
|
icon: 'storage',
|
||||||
image: '/assets/services/nas-storage.png'
|
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 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 [activeCategory, setActiveCategory] = useState('All');
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
||||||
|
|
||||||
// Reset refs on render to handle filtering updates
|
// Determine if we should be in "preview mode" (showing only a subset)
|
||||||
imagesRef.current = [];
|
// 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
|
||||||
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
||||||
|
|
||||||
const displayedServices = preview ? servicesData.slice(0, 3) : filteredServices;
|
const displayedServices = useMemo(() => {
|
||||||
|
if (isRestrictedView) {
|
||||||
useLayoutEffect(() => {
|
if (featuredIds && featuredIds.length > 0) {
|
||||||
const ctx = gsap.context(() => {
|
// Sort the services to match the order of featuredIds
|
||||||
imagesRef.current.forEach((imgWrapper) => {
|
return featuredIds
|
||||||
if (!imgWrapper) return;
|
.map(id => servicesData.find(s => s.id === id))
|
||||||
|
.filter((s): s is typeof servicesData[0] => s !== undefined);
|
||||||
gsap.to(imgWrapper, {
|
}
|
||||||
yPercent: 30,
|
// Fallback to first 3 if no IDs but preview is true
|
||||||
ease: "none",
|
return servicesData.slice(0, 3);
|
||||||
scrollTrigger: {
|
}
|
||||||
trigger: imgWrapper.closest('.group'),
|
// Show all (filtered by category)
|
||||||
start: "top bottom",
|
return filteredByCategory;
|
||||||
end: "bottom top",
|
}, [isRestrictedView, featuredIds, filteredByCategory]);
|
||||||
scrub: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, containerRef);
|
|
||||||
|
|
||||||
return () => ctx.revert();
|
|
||||||
}, [filteredServices]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
|
|
@ -127,32 +150,35 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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 - 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. */}
|
||||||
{categories.map((cat) => (
|
{!isRestrictedView && (
|
||||||
<button
|
<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">
|
||||||
key={cat}
|
{categories.map((cat) => (
|
||||||
onClick={() => setActiveCategory(cat)}
|
<button
|
||||||
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
|
key={cat}
|
||||||
? 'text-gray-900 dark:text-white'
|
onClick={() => setActiveCategory(cat)}
|
||||||
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
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
|
{cat}
|
||||||
layoutId="activeTab"
|
{activeCategory === cat && (
|
||||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
<motion.div
|
||||||
/>
|
layoutId="activeTab"
|
||||||
)}
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
||||||
</button>
|
/>
|
||||||
))}
|
)}
|
||||||
</div>
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
{filteredServices.map((service, index) => (
|
{displayedServices.map((service) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={service.id}
|
key={service.id}
|
||||||
layout
|
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"
|
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 */}
|
{/* Image Container */}
|
||||||
<div className="h-64 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
<div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
||||||
{/* Parallax Wrapper */}
|
<img
|
||||||
<div
|
src={service.image}
|
||||||
ref={el => { if (el) imagesRef.current.push(el); }}
|
alt={service.title}
|
||||||
className="w-full h-[140%] -mt-[20%]"
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
|
||||||
>
|
/>
|
||||||
<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="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 relative">
|
<div className="p-4 relative">
|
||||||
<motion.div
|
<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" }}
|
whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span>
|
<span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<h3 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
|
<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-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</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
|
Learn More <motion.span
|
||||||
className="material-symbols-outlined text-xs ml-1"
|
className="material-symbols-outlined text-xs ml-1"
|
||||||
animate={{ x: [0, 5, 0] }}
|
animate={{ x: [0, 5, 0] }}
|
||||||
|
|
@ -204,16 +224,18 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{preview && (
|
{isRestrictedView && (
|
||||||
<div className="mt-12 text-center">
|
<div className="mt-12 text-center">
|
||||||
<a
|
<button
|
||||||
href="/services"
|
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"
|
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>
|
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
16
index.html
|
|
@ -65,6 +65,22 @@
|
||||||
.lenis.lenis-scrolling iframe {
|
.lenis.lenis-scrolling iframe {
|
||||||
pointer-events: none;
|
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>
|
</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">
|
<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>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1475,6 +1476,19 @@
|
||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/gsap": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
||||||
|
|
@ -1690,6 +1704,16 @@
|
||||||
"react-dom": ">=18"
|
"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": {
|
"node_modules/rollup": {
|
||||||
"version": "4.55.1",
|
"version": "4.55.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||||
|
|
@ -1790,6 +1814,510 @@
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"generate:seo": "npx tsx scripts/generate-sitemap.ts && npx tsx scripts/generate-robots.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@studio-freight/lenis": "^1.0.42",
|
"@studio-freight/lenis": "^1.0.42",
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 703 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 733 KiB |
|
After Width: | Height: | Size: 842 KiB |
|
After Width: | Height: | Size: 701 KiB |
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 734 KiB |
|
After Width: | Height: | Size: 692 KiB |
|
After Width: | Height: | Size: 770 KiB |
|
After Width: | Height: | Size: 811 KiB |
|
After Width: | Height: | Size: 757 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 929 KiB |
|
|
@ -0,0 +1,6 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Disallow: /admin
|
||||||
|
Disallow: /api
|
||||||
|
|
||||||
|
Sitemap: https://bayareait.services/sitemap.xml
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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}`);
|
||||||
|
|
@ -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}`);
|
||||||
|
|
@ -1,36 +1,8 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import Contact from '../../components/Contact';
|
import Contact from '../../components/Contact';
|
||||||
|
import { blogPostData } from '../data/seoData';
|
||||||
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 = () => {
|
const BlogPage: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -52,46 +24,51 @@ const BlogPage: React.FC = () => {
|
||||||
|
|
||||||
<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))]">
|
<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">
|
<div className="max-w-5xl mx-auto space-y-16">
|
||||||
{posts.map((post) => (
|
{blogPostData.map((post) => (
|
||||||
<motion.div
|
<Link
|
||||||
key={post.id}
|
key={post.id}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
to={`/${post.slug}`}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
className="block"
|
||||||
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">
|
<motion.div
|
||||||
<img
|
initial={{ opacity: 0, y: 20 }}
|
||||||
src={post.image}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
alt={post.title}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
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="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">
|
<div className="h-64 md:h-auto overflow-hidden relative">
|
||||||
{post.category}
|
<img
|
||||||
</span>
|
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>
|
||||||
</div>
|
<div className="p-8 md:p-12 flex flex-col justify-center">
|
||||||
<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">
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
|
<span>Jan 2026</span>
|
||||||
<span>{post.date}</span>
|
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
||||||
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
<span>8 min read</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.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>
|
</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">
|
</motion.div>
|
||||||
{post.title}
|
</Link>
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -6,20 +6,84 @@ import Process from '../../components/Process';
|
||||||
import Blog from '../../components/Blog';
|
import Blog from '../../components/Blog';
|
||||||
import Testimonials from '../../components/Testimonials';
|
import Testimonials from '../../components/Testimonials';
|
||||||
import CTA from '../../components/CTA';
|
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 = () => {
|
const HomePage: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo(0, 0);
|
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 (
|
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 />
|
<Hero />
|
||||||
<Mission />
|
<Mission />
|
||||||
<Services preview={true} />
|
<Services preview={true} />
|
||||||
<Process />
|
<Process />
|
||||||
<Blog />
|
<Blog />
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
|
<AreasWeServe />
|
||||||
|
<FAQ items={locationData[0].faq} />
|
||||||
<CTA />
|
<CTA />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -206,6 +206,7 @@ const ServiceModal: React.FC<{ service: typeof services[0] | null; onClose: () =
|
||||||
exit={{ scale: 0.9, opacity: 0 }}
|
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"
|
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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
data-lenis-prevent
|
||||||
>
|
>
|
||||||
{/* Close Button - Sticky and distinct */}
|
{/* Close Button - Sticky and distinct */}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||