Complete SEO overhaul
31
App.tsx
|
|
@ -12,6 +12,10 @@ import AboutPage from './src/pages/AboutPage';
|
|||
import ServicesPage from './src/pages/ServicesPage';
|
||||
import BlogPage from './src/pages/BlogPage';
|
||||
import ContactPage from './src/pages/ContactPage';
|
||||
import LocationPage from './src/pages/LocationPage';
|
||||
import ServicePage from './src/pages/ServicePage';
|
||||
import BlogPostPage from './src/pages/BlogPostPage';
|
||||
import { locationData, serviceData, blogPostData } from './src/data/seoData';
|
||||
|
||||
// Register GSAP plugins globally
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||
|
|
@ -69,6 +73,33 @@ const AppContent: React.FC = () => {
|
|||
<Route path="/services" element={<ServicesPage />} />
|
||||
<Route path="/blog" element={<BlogPage />} />
|
||||
<Route path="/contact" element={<ContactPage />} />
|
||||
|
||||
{/* SEO Location Pages */}
|
||||
{locationData.map((data) => (
|
||||
<Route
|
||||
key={data.slug}
|
||||
path={`/${data.slug}`}
|
||||
element={<LocationPage data={data} />}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* SEO Service Pages */}
|
||||
{serviceData.map((data) => (
|
||||
<Route
|
||||
key={data.slug}
|
||||
path={`/${data.slug}`}
|
||||
element={<ServicePage data={data} />}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Authority Blog Posts */}
|
||||
{blogPostData.map((data) => (
|
||||
<Route
|
||||
key={data.slug}
|
||||
path={`/${data.slug}`}
|
||||
element={<BlogPostPage data={data} />}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
|
|
|
|||
|
|
@ -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 gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
|
@ -69,45 +69,68 @@ const servicesData = [
|
|||
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
|
||||
icon: 'storage',
|
||||
image: '/assets/services/nas-storage.png'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
category: 'IT Infrastructure',
|
||||
title: 'Business IT Support',
|
||||
description: 'Comprehensive IT support for businesses, including help desk, maintenance, and strategic planning.',
|
||||
icon: 'business_center',
|
||||
image: '/assets/services/business-it.png'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
category: 'IT Infrastructure',
|
||||
title: 'IT Help Desk',
|
||||
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
|
||||
icon: 'support_agent',
|
||||
image: '/assets/services/help-desk.png'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
category: 'IT Infrastructure',
|
||||
title: 'Managed IT Services',
|
||||
description: 'Proactive monitoring, security, and management of your entire IT infrastructure for a fixed monthly fee.',
|
||||
icon: 'admin_panel_settings',
|
||||
image: '/assets/services/managed-it.png'
|
||||
}
|
||||
];
|
||||
|
||||
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking'];
|
||||
|
||||
const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
|
||||
interface ServicesProps {
|
||||
preview?: boolean;
|
||||
featuredIds?: number[];
|
||||
}
|
||||
|
||||
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
|
||||
const [activeCategory, setActiveCategory] = useState('All');
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
// Reset refs on render to handle filtering updates
|
||||
imagesRef.current = [];
|
||||
// Determine if we should be in "preview mode" (showing only a subset)
|
||||
// This applies if preview is true OR if featuredIds are provided and we haven't clicked "Show More"
|
||||
const isRestrictedView = (preview || featuredIds) && !showAll;
|
||||
|
||||
const filteredServices = activeCategory === 'All'
|
||||
// Filter services based on category first (unless in restricted view with specific IDs, where we might want to ignore category or just show the specific ones)
|
||||
const filteredByCategory = activeCategory === 'All'
|
||||
? servicesData
|
||||
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
||||
|
||||
const displayedServices = preview ? servicesData.slice(0, 3) : filteredServices;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
imagesRef.current.forEach((imgWrapper) => {
|
||||
if (!imgWrapper) return;
|
||||
|
||||
gsap.to(imgWrapper, {
|
||||
yPercent: 30,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: imgWrapper.closest('.group'),
|
||||
start: "top bottom",
|
||||
end: "bottom top",
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}, containerRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [filteredServices]);
|
||||
const displayedServices = useMemo(() => {
|
||||
if (isRestrictedView) {
|
||||
if (featuredIds && featuredIds.length > 0) {
|
||||
// Sort the services to match the order of featuredIds
|
||||
return featuredIds
|
||||
.map(id => servicesData.find(s => s.id === id))
|
||||
.filter((s): s is typeof servicesData[0] => s !== undefined);
|
||||
}
|
||||
// Fallback to first 3 if no IDs but preview is true
|
||||
return servicesData.slice(0, 3);
|
||||
}
|
||||
// Show all (filtered by category)
|
||||
return filteredByCategory;
|
||||
}, [isRestrictedView, featuredIds, filteredByCategory]);
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
|
|
@ -127,32 +150,35 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
|
|||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
|
||||
? 'text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
{activeCategory === cat && (
|
||||
<motion.div
|
||||
layoutId="activeTab"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Categories - Hide in restricted view to keep it clean, or keep it? User said "mach nur das 3 services angezeigt werden". usually categories are for the full list. */}
|
||||
{!isRestrictedView && (
|
||||
<div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
|
||||
? 'text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
{activeCategory === cat && (
|
||||
<motion.div
|
||||
layoutId="activeTab"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredServices.map((service, index) => (
|
||||
{displayedServices.map((service) => (
|
||||
<motion.div
|
||||
key={service.id}
|
||||
layout
|
||||
|
|
@ -164,34 +190,28 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
|
|||
className="group relative bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden hover:border-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300"
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="h-64 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
||||
{/* Parallax Wrapper */}
|
||||
<div
|
||||
ref={el => { if (el) imagesRef.current.push(el); }}
|
||||
className="w-full h-[140%] -mt-[20%]"
|
||||
>
|
||||
<img
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
||||
<img
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 relative">
|
||||
<div className="p-4 relative">
|
||||
<motion.div
|
||||
className="w-10 h-10 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-4 border border-gray-200 dark:border-white/10"
|
||||
className="w-8 h-8 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-3 border border-gray-200 dark:border-white/10"
|
||||
whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span>
|
||||
</motion.div>
|
||||
<h3 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-4">
|
||||
<h3 className="font-display text-lg font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
|
||||
{service.description}
|
||||
</p>
|
||||
<a href="#" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
|
||||
<a href="/services" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
|
||||
Learn More <motion.span
|
||||
className="material-symbols-outlined text-xs ml-1"
|
||||
animate={{ x: [0, 5, 0] }}
|
||||
|
|
@ -204,16 +224,18 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
|
|||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{preview && (
|
||||
{isRestrictedView && (
|
||||
<div className="mt-12 text-center">
|
||||
<a
|
||||
href="/services"
|
||||
<button
|
||||
onClick={() => setShowAll(true)}
|
||||
className="inline-flex items-center gap-2 px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
View all services <span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</a>
|
||||
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* If we are showing all and originally had a restricted view, maybe show a "Show Less" but user didn't ask for it. The user said "then all are shown". */}
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
|
|
|
|||
16
index.html
|
|
@ -65,6 +65,22 @@
|
|||
.lenis.lenis-scrolling iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
<body class="bg-background-light dark:bg-background-dark text-gray-900 dark:text-white font-sans antialiased selection:bg-white selection:text-black transition-colors duration-300">
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
|
|
@ -1475,6 +1476,19 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gsap": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
||||
|
|
@ -1690,6 +1704,16 @@
|
|||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||
|
|
@ -1790,6 +1814,510 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.2",
|
||||
"@esbuild/android-arm": "0.27.2",
|
||||
"@esbuild/android-arm64": "0.27.2",
|
||||
"@esbuild/android-x64": "0.27.2",
|
||||
"@esbuild/darwin-arm64": "0.27.2",
|
||||
"@esbuild/darwin-x64": "0.27.2",
|
||||
"@esbuild/freebsd-arm64": "0.27.2",
|
||||
"@esbuild/freebsd-x64": "0.27.2",
|
||||
"@esbuild/linux-arm": "0.27.2",
|
||||
"@esbuild/linux-arm64": "0.27.2",
|
||||
"@esbuild/linux-ia32": "0.27.2",
|
||||
"@esbuild/linux-loong64": "0.27.2",
|
||||
"@esbuild/linux-mips64el": "0.27.2",
|
||||
"@esbuild/linux-ppc64": "0.27.2",
|
||||
"@esbuild/linux-riscv64": "0.27.2",
|
||||
"@esbuild/linux-s390x": "0.27.2",
|
||||
"@esbuild/linux-x64": "0.27.2",
|
||||
"@esbuild/netbsd-arm64": "0.27.2",
|
||||
"@esbuild/netbsd-x64": "0.27.2",
|
||||
"@esbuild/openbsd-arm64": "0.27.2",
|
||||
"@esbuild/openbsd-x64": "0.27.2",
|
||||
"@esbuild/openharmony-arm64": "0.27.2",
|
||||
"@esbuild/sunos-x64": "0.27.2",
|
||||
"@esbuild/win32-arm64": "0.27.2",
|
||||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"generate:seo": "npx tsx scripts/generate-sitemap.ts && npx tsx scripts/generate-robots.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@studio-freight/lenis": "^1.0.42",
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
|
|
|
|||
|
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 { motion } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Contact from '../../components/Contact';
|
||||
|
||||
const posts = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Upgrade your HDD to SSD for a big speed boost',
|
||||
excerpt: 'A practical checklist for Corpus Christi business owners considering SSD upgrades, including before/after performance comparisons and cost analysis.',
|
||||
image: '/assets/services/desktop-hardware.png',
|
||||
category: 'Hardware',
|
||||
readTime: '5 min read',
|
||||
date: 'Jan 15, 2026'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Secure your corporate network access with WireGuard VPN',
|
||||
excerpt: 'Learn why Corpus Christi businesses are switching to WireGuard VPN for faster, more secure remote access, and how to implement it properly in the Coastal Bend.',
|
||||
image: '/assets/services/vpn-setup.png',
|
||||
category: 'Security',
|
||||
readTime: '7 min read',
|
||||
date: 'Jan 10, 2026'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'What comprehensive IT support looks like for SMBs',
|
||||
excerpt: 'Understanding the full scope of managed IT services for Corpus Christi small businesses: from hardware and network infrastructure to virtualization and helpdesk support.',
|
||||
image: '/assets/services/network-infrastructure.png',
|
||||
category: 'Strategy',
|
||||
readTime: '6 min read',
|
||||
date: 'Jan 05, 2026'
|
||||
}
|
||||
];
|
||||
import { blogPostData } from '../data/seoData';
|
||||
|
||||
const BlogPage: React.FC = () => {
|
||||
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))]">
|
||||
<div className="max-w-5xl mx-auto space-y-16">
|
||||
{posts.map((post) => (
|
||||
<motion.div
|
||||
{blogPostData.map((post) => (
|
||||
<Link
|
||||
key={post.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
whileHover={{ y: -5 }}
|
||||
className="group grid md:grid-cols-2 gap-0 bg-white dark:bg-[#161616] rounded-3xl overflow-hidden shadow-lg border border-gray-100 dark:border-white/5 hover:shadow-2xl hover:shadow-blue-900/10 transition-all duration-300"
|
||||
to={`/${post.slug}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="h-64 md:h-auto overflow-hidden relative">
|
||||
<img
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className="px-3 py-1 bg-black/70 backdrop-blur-md text-white text-xs font-bold rounded-full border border-white/20">
|
||||
{post.category}
|
||||
</span>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
whileHover={{ y: -5 }}
|
||||
className="group grid md:grid-cols-2 gap-0 bg-white dark:bg-[#161616] rounded-3xl overflow-hidden shadow-lg border border-gray-100 dark:border-white/5 hover:shadow-2xl hover:shadow-blue-900/10 transition-all duration-300"
|
||||
>
|
||||
<div className="h-64 md:h-auto overflow-hidden relative">
|
||||
<img
|
||||
src={post.image || '/images/blog/default.png'}
|
||||
alt={post.h1}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className="px-3 py-1 bg-black/70 backdrop-blur-md text-white text-xs font-bold rounded-full border border-white/20">
|
||||
{post.category === 'authority' ? 'IT Insights' : 'Local Services'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 md:p-12 flex flex-col justify-center">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
<span>{post.date}</span>
|
||||
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
||||
<span>{post.readTime}</span>
|
||||
<div className="p-8 md:p-12 flex flex-col justify-center">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
<span>Jan 2026</span>
|
||||
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
||||
<span>8 min read</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{post.h1}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-8">
|
||||
{post.description}
|
||||
</p>
|
||||
<div className="mt-auto">
|
||||
<span className="inline-flex items-center gap-2 font-bold text-blue-600 dark:text-white group-hover:gap-3 transition-all">
|
||||
Read article <span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-8">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
<div className="mt-auto">
|
||||
<span className="inline-flex items-center gap-2 font-bold text-blue-600 dark:text-white group-hover:gap-3 transition-all">
|
||||
Read article <span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</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 Testimonials from '../../components/Testimonials';
|
||||
import CTA from '../../components/CTA';
|
||||
import SEO from '../../components/SEO';
|
||||
import FAQ from '../../components/FAQ';
|
||||
import AreasWeServe from '../../components/AreasWeServe';
|
||||
import { locationData } from '../data/seoData';
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
// Enhanced LocalBusiness Schema per SEO plan
|
||||
const schema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ITService",
|
||||
"name": "Bay Area IT Services",
|
||||
"image": "https://bayarea-cc.com/logo.png",
|
||||
"@id": "https://bayarea-cc.com",
|
||||
"url": "https://bayarea-cc.com",
|
||||
"telephone": "+1-361-XXX-XXXX", // TODO: Replace with actual phone
|
||||
"priceRange": "$$",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "[YOUR STREET]", // TODO: Add actual address
|
||||
"addressLocality": "Corpus Christi",
|
||||
"addressRegion": "TX",
|
||||
"postalCode": "[YOUR ZIP]", // TODO: Add actual ZIP
|
||||
"addressCountry": "US"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": 27.800583,
|
||||
"longitude": -97.39638
|
||||
},
|
||||
"areaServed": [
|
||||
{ "@type": "City", "name": "Corpus Christi" },
|
||||
{ "@type": "City", "name": "Portland" },
|
||||
{ "@type": "City", "name": "Rockport" },
|
||||
{ "@type": "City", "name": "Aransas Pass" },
|
||||
{ "@type": "City", "name": "Kingsville" }
|
||||
],
|
||||
"serviceType": [
|
||||
"IT Support",
|
||||
"Business IT Support",
|
||||
"Outsourced IT Services",
|
||||
"Computer Network Support",
|
||||
"Cyber Security"
|
||||
],
|
||||
"openingHoursSpecification": {
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday"
|
||||
],
|
||||
"opens": "08:00",
|
||||
"closes": "18:00"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title="IT Service & IT Support for Businesses in Corpus Christi, TX"
|
||||
description="Reliable IT support and IT services for businesses in Corpus Christi, TX. Fast response, outsourced IT support & help desk solutions. Call now."
|
||||
keywords={["IT Service", "IT Support", "Corpus Christi", "Business IT Support"]}
|
||||
canonicalUrl={window.location.href}
|
||||
schema={schema}
|
||||
/>
|
||||
<Hero />
|
||||
<Mission />
|
||||
<Services preview={true} />
|
||||
<Process />
|
||||
<Blog />
|
||||
<Testimonials />
|
||||
<AreasWeServe />
|
||||
<FAQ items={locationData[0].faq} />
|
||||
<CTA />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
className="bg-[#1a1a1a] border border-white/10 rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto relative shadow-2xl custom-scrollbar"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-lenis-prevent
|
||||
>
|
||||
{/* Close Button - Sticky and distinct */}
|
||||
<button
|
||||
|
|
|
|||