Neue services
166
.agents/product-marketing-context.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# Product Marketing Context
|
||||||
|
|
||||||
|
*Last updated: 2026-03-25*
|
||||||
|
|
||||||
|
## Product Overview
|
||||||
|
**One-liner:** Bay Area IT is a local managed IT and business technology partner for small and mid-sized businesses in Corpus Christi and the Coastal Bend.
|
||||||
|
|
||||||
|
**What it does:** The company provides ongoing IT support, help desk, network infrastructure, business email, device support, security basics, backups, and related technology services for local businesses. It also offers adjacent services such as web design, shared drive setup, printer/scanner installation, and hardware procurement. The positioning throughout the site emphasizes reliable day-to-day operations, fast support, and a local partner that can help remotely or on-site.
|
||||||
|
|
||||||
|
**Product category:** Managed IT services, IT support, outsourced IT support, IT help desk, business technology services for SMBs.
|
||||||
|
|
||||||
|
**Product type:** Local B2B service business.
|
||||||
|
|
||||||
|
**Business model:** Service-led business with monthly recurring support plans plus project-based services. Pricing signals on the site suggest transparent monthly pricing for support and per-unit pricing for some offers (example: business email at `$5` per inbox). Primary conversion is a free 20-minute assessment / consultation.
|
||||||
|
|
||||||
|
## Target Audience
|
||||||
|
**Target companies:** Small and mid-sized businesses in Corpus Christi and the broader Coastal Bend, including Portland, Rockport, Aransas Pass, Kingsville, and Port Aransas. Strong fit for offices, service businesses, healthcare-related practices, growing teams, and companies without a full in-house IT department.
|
||||||
|
|
||||||
|
**Decision-makers:** Business owners, office managers, operations leaders, and IT-responsible managers at SMBs. Secondary audiences likely include executive assistants, practice managers, and internal technical contacts who influence vendor selection.
|
||||||
|
|
||||||
|
**Primary use case:** Outsource day-to-day IT management so the business can reduce downtime, improve security, and keep employees productive without building a full internal IT team.
|
||||||
|
|
||||||
|
**Jobs to be done:**
|
||||||
|
- Keep our systems, email, devices, Wi-Fi, and users working reliably every day.
|
||||||
|
- Give employees a clear support channel so small issues stop turning into business delays.
|
||||||
|
- Put practical security, backup, and monitoring in place without overcomplicating the environment.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Ongoing managed IT support for SMBs without in-house IT
|
||||||
|
- Remote-first help desk with on-site escalation when needed
|
||||||
|
- Business email setup, migration, and support
|
||||||
|
- Network cleanup, Wi-Fi improvement, firewall/router/switch management
|
||||||
|
- New user onboarding, device setup, and account/access support
|
||||||
|
- Web design and domain/DNS support for small local businesses
|
||||||
|
|
||||||
|
## Personas
|
||||||
|
| Persona | Cares about | Challenge | Value we promise |
|
||||||
|
|---------|-------------|-----------|------------------|
|
||||||
|
| Business owner / President | Predictable costs, fewer disruptions, trust in provider, fast escalation | They are tired of acting as part-time IT and losing time to avoidable issues | We take IT ownership off their plate with clear communication, local accountability, and practical support |
|
||||||
|
| Office manager / Operations lead | Employee productivity, quick fixes, simple process, dependable vendor | Staff lose time to recurring tickets, login issues, printer/email/network friction | We give your team a responsive help desk and structured support workflow that keeps work moving |
|
||||||
|
| Practice manager / Admin lead | Reliability, compliance-adjacent security basics, smooth communication systems | Email, devices, and access issues disrupt client or patient-facing work | We stabilize the environment and implement simple security and backup foundations |
|
||||||
|
| Internal technical contact | Competence, clean implementation, sane network/security setup | They need outside help without inheriting chaos or vague recommendations | We provide hands-on implementation, monitoring, and support without unnecessary complexity |
|
||||||
|
| Financial buyer | Cost control, ROI, avoiding headcount, lower risk | Hiring full-time IT is expensive and overkill for many SMBs | Outsourced IT gives predictable monthly pricing and broader coverage than a single in-house hire |
|
||||||
|
|
||||||
|
## Problems & Pain Points
|
||||||
|
**Core problem:** Business technology has become unpredictable, reactive, and distracting. Employees hit avoidable issues, support requests pile up, and owners or managers end up spending time managing IT instead of running the business.
|
||||||
|
|
||||||
|
**Why alternatives fall short:**
|
||||||
|
- Break-fix support is reactive and does not create ongoing stability
|
||||||
|
- Remote-only national providers can feel slow, generic, and disconnected from local urgency
|
||||||
|
- A single in-house generalist is expensive and may not cover the full range of needs
|
||||||
|
- DIY setups for email, networking, security, and backups create hidden fragility
|
||||||
|
|
||||||
|
**What it costs them:** Lost productivity, downtime, delayed customer response, missed emails, security exposure, recurring employee frustration, and management time pulled away from growth.
|
||||||
|
|
||||||
|
**Emotional tension:** Frustration, lack of confidence in systems, fear of outages or security incidents, and fatigue from being forced to “figure IT out” internally.
|
||||||
|
|
||||||
|
## Competitive Landscape
|
||||||
|
**Direct:** Local MSPs and IT support providers in Corpus Christi / Coastal Bend — often positioned similarly on help desk, managed IT, and outsourced support; likely to fall short on responsiveness, communication clarity, or service breadth if they are inconsistent.
|
||||||
|
|
||||||
|
**Secondary:** Freelance IT technicians, break-fix computer repair shops, and one-person outsourced support providers — cheaper upfront but often reactive, less structured, and weaker on ongoing monitoring, security baselines, and business process support.
|
||||||
|
|
||||||
|
**Indirect:** Hiring an internal IT employee, relying on a tech-savvy office employee, or using vendor support from Microsoft/Google/ISP/hardware providers — these options are fragmented, slower for cross-system problems, and usually do not provide clear ownership of the full environment.
|
||||||
|
|
||||||
|
## Differentiation
|
||||||
|
**Key differentiators:**
|
||||||
|
- Local provider based in Corpus Christi with regional service coverage
|
||||||
|
- Remote-first support with on-site availability when needed
|
||||||
|
- Broad business technology scope: help desk, email, networking, devices, security basics, backups, web, DNS/domain support
|
||||||
|
- Clear, practical positioning instead of overly technical jargon
|
||||||
|
- Free 20-minute assessment as a low-friction first step
|
||||||
|
|
||||||
|
**How we do it differently:** We position the company as a local IT partner that combines fast support, hands-on implementation, and practical business judgment. The site consistently emphasizes reliable operations, simple explanations, and support that fits SMB reality rather than enterprise complexity.
|
||||||
|
|
||||||
|
**Why that's better:** SMB buyers get one accountable partner, faster issue resolution, lower management overhead, and support that balances remote speed with local presence.
|
||||||
|
|
||||||
|
**Why customers choose us:** They want predictable help, local accountability, straightforward communication, and a provider who can support everyday business operations instead of only isolated technical tasks.
|
||||||
|
|
||||||
|
## Objections
|
||||||
|
| Objection | Response |
|
||||||
|
|-----------|----------|
|
||||||
|
| “We’re too small to need managed IT.” | Small businesses are often the most disrupted by recurring tech issues because they have less internal coverage. Outsourced support gives you structure and expertise without hiring a full-time team. |
|
||||||
|
| “We already have someone who helps with IT.” | That can work until issues span email, networking, security, users, and vendors at the same time. We provide broader coverage, clearer ownership, and ongoing support instead of ad hoc fixes. |
|
||||||
|
| “A remote provider or break-fix shop is cheaper.” | Lower upfront cost usually means slower response, less accountability, and more recurring issues. The site’s positioning is around reducing downtime and management distraction, not just closing tickets cheaply. |
|
||||||
|
|
||||||
|
**Anti-persona:** Large enterprises with internal IT departments and complex procurement, buyers shopping only on the lowest hourly rate, and organizations that want purely transactional break-fix work with no ongoing relationship.
|
||||||
|
|
||||||
|
## Switching Dynamics
|
||||||
|
**Push:** Recurring downtime, slow systems, unresolved support tickets, weak Wi-Fi, email problems, unclear ownership, and security/backups that feel too risky to ignore.
|
||||||
|
|
||||||
|
**Pull:** Local support, fast response, clear communication, monthly support options, practical security baseline, and an easy first step via a free assessment.
|
||||||
|
|
||||||
|
**Habit:** Existing vendor relationships, internal workarounds, “good enough” support, reluctance to document the current environment, and the inertia of staying reactive.
|
||||||
|
|
||||||
|
**Anxiety:** Fear of disruption during migration, concern about cost, worry that the new provider will overcomplicate things, and uncertainty about whether a local MSP will actually be more responsive.
|
||||||
|
|
||||||
|
## Customer Language
|
||||||
|
**How they describe the problem:**
|
||||||
|
- “The internet is slow.”
|
||||||
|
- “The system is glitchy.”
|
||||||
|
- “Our email stopped working.”
|
||||||
|
- “Support requests never end.”
|
||||||
|
- “I’m tired of playing part-time IT.”
|
||||||
|
- “We need reliable IT without hiring in-house staff.”
|
||||||
|
|
||||||
|
**How they describe us:**
|
||||||
|
- “Local IT support”
|
||||||
|
- “Outsourced IT support”
|
||||||
|
- “Fast remote assistance with on-site support when needed”
|
||||||
|
- “Reliable IT partner”
|
||||||
|
- “Help desk for employees”
|
||||||
|
|
||||||
|
**Words to use:** reliable, local, fast response, outsourced IT support, help desk, remote support, on-site when needed, security baseline, backup monitoring, predictable monthly pricing, practical, clear, business-focused.
|
||||||
|
|
||||||
|
**Words to avoid:** revolutionary, cutting-edge for its own sake, AI-first, disruption, synergy, enterprise transformation, generic “digital innovation” language.
|
||||||
|
|
||||||
|
**Glossary:**
|
||||||
|
| Term | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| Managed IT / ongoing IT support | Recurring support relationship covering maintenance, issue resolution, and operational stability |
|
||||||
|
| Help desk | Day-to-day employee support for access, software, device, and common IT issues |
|
||||||
|
| Security baseline | Practical protection measures like MFA, patching, endpoint protection, and backup basics |
|
||||||
|
| Remote-first support | Resolving most issues quickly without waiting for an on-site visit |
|
||||||
|
| On-site support | Physical support for hardware, installations, and network troubleshooting when required |
|
||||||
|
|
||||||
|
## Brand Voice
|
||||||
|
**Tone:** Professional, direct, practical, reassuring.
|
||||||
|
|
||||||
|
**Style:** Conversational but credible. Explains technical work in business terms. Avoids jargon-heavy positioning and stays grounded in outcomes like uptime, productivity, and reliability.
|
||||||
|
|
||||||
|
**Personality:** Local, dependable, clear, experienced, no-nonsense.
|
||||||
|
|
||||||
|
## Proof Points
|
||||||
|
**Metrics:**
|
||||||
|
- 25+ years serving the region appears repeatedly across location/service pages
|
||||||
|
- 30+ local businesses supported appears across multiple pages
|
||||||
|
- 99.9% uptime and `<2 min` response time appear on the About page
|
||||||
|
- Assessments can begin within 48 hours
|
||||||
|
- 24/7 emergency support / monitoring is referenced in contact and CTA copy
|
||||||
|
|
||||||
|
**Customers:** No named logos surfaced in the repo copy reviewed. The site references local businesses across the Coastal Bend rather than public logos.
|
||||||
|
|
||||||
|
**Testimonials:**
|
||||||
|
> No direct testimonial quotes were found in the repo sections reviewed. Add real customer quotes here when available.
|
||||||
|
|
||||||
|
**Value themes:**
|
||||||
|
| Theme | Proof |
|
||||||
|
|-------|-------|
|
||||||
|
| Local trust | Corpus Christi address, repeated local positioning, service-area pages for Coastal Bend cities |
|
||||||
|
| Reliability | 99.9% uptime claim, remote-first help desk, proactive maintenance and monitoring language |
|
||||||
|
| Experience | 25+ years claim across pages; long-standing regional presence |
|
||||||
|
| Business fit | Positioning for small and mid-sized businesses without in-house IT |
|
||||||
|
| Speed | Fast response, emergency support, assessments within 48 hours |
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
**Business goal:** Generate qualified local SMB leads for managed IT, help desk, business email, networking, and related technology services.
|
||||||
|
|
||||||
|
**Conversion action:** Book a free 20-minute assessment / consultation or submit a contact inquiry / call the office.
|
||||||
|
|
||||||
|
**Current metrics:** Not fully established from the repo alone. Repo signals include claims around 30+ businesses supported, 99.9% uptime, fast response, and 48-hour assessment turnaround. Customer-count references conflict in the codebase and should be normalized before using in external collateral.
|
||||||
|
|
||||||
|
## Open Questions / Conflicts To Resolve
|
||||||
|
- Brand name is inconsistent: `Bay Area Affiliates`, `Bay Area IT`, and `Bay Area IT Services` all appear in the repo.
|
||||||
|
- Customer count is inconsistent: some pages say `30+ local businesses`, while `AboutPage` also says `over 150 businesses`.
|
||||||
|
- Founding timeline is inconsistent: timeline starts in `2000`, but hero/about copy also says `Since 2010`.
|
||||||
|
- Pricing clarity is partial: one service has explicit `$5 per inbox` pricing, while broader managed IT pricing is only described as transparent monthly pricing.
|
||||||
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.agents
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
bullet
|
||||||
|
ellipsis
|
||||||
|
em
|
||||||
|
left
|
||||||
|
right
|
||||||
48
.gitignore
vendored
@@ -1,24 +1,24 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|||||||
177
App.tsx
@@ -1,65 +1,93 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { Suspense, useEffect, useRef } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||||
import Lenis from '@studio-freight/lenis';
|
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
||||||
import Navbar from './components/Navbar';
|
import Navbar from './components/Navbar';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import BackToTop from './components/BackToTop';
|
import BackToTop from './components/BackToTop';
|
||||||
import HomePage from './src/pages/HomePage';
|
import { BlogSeoRoute, LocationSeoRoute, ServiceSeoRoute, blogRoutes, locationRoutes, serviceRoutes, legacyRedirects } from './src/routes/seoRoutes';
|
||||||
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
|
// Register GSAP plugins globally
|
||||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||||
|
|
||||||
|
import HomePage from './src/pages/HomePage';
|
||||||
|
const AboutPage = React.lazy(() => import('./src/pages/AboutPage'));
|
||||||
|
const ServicesPage = React.lazy(() => import('./src/pages/ServicesPage'));
|
||||||
|
const BlogPage = React.lazy(() => import('./src/pages/BlogPage'));
|
||||||
|
const ContactPage = React.lazy(() => import('./src/pages/ContactPage'));
|
||||||
|
const LocationsPage = React.lazy(() => import('./src/pages/LocationsPage'));
|
||||||
|
const PrivacyPolicyPage = React.lazy(() => import('./src/pages/PrivacyPolicyPage'));
|
||||||
|
const TermsOfServicePage = React.lazy(() => import('./src/pages/TermsOfServicePage'));
|
||||||
|
|
||||||
// Grain Overlay Component
|
// Grain Overlay Component
|
||||||
const GrainOverlay = () => (
|
const GrainOverlay = () => (
|
||||||
<div className="fixed inset-0 w-full h-full pointer-events-none z-50 opacity-50 dark:opacity-100 bg-grain mix-blend-overlay"></div>
|
<div className="fixed inset-0 w-full h-full pointer-events-none z-50 opacity-50 dark:opacity-100 bg-grain mix-blend-overlay"></div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const RouteFallback = () => (
|
||||||
|
<div className="min-h-[50vh] flex items-center justify-center px-6 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Loading page...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const lenisRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize Lenis for smooth scrolling
|
if (typeof window === 'undefined') {
|
||||||
const lenis = new Lenis({
|
return;
|
||||||
duration: 1.2,
|
}
|
||||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
|
||||||
direction: 'vertical',
|
|
||||||
gestureDirection: 'vertical',
|
|
||||||
smooth: true,
|
|
||||||
smoothTouch: false,
|
|
||||||
touchMultiplier: 2,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
// Synchronize Lenis with GSAP ScrollTrigger
|
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
lenis.on('scroll', ScrollTrigger.update);
|
const finePointer = window.matchMedia('(pointer: fine) and (hover: hover)');
|
||||||
|
|
||||||
const ticker = (time: number) => {
|
if (prefersReducedMotion.matches || !finePointer.matches) {
|
||||||
lenis.raf(time * 1000);
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Use GSAP ticker for smoother animation loop integration
|
let active = true;
|
||||||
gsap.ticker.add(ticker);
|
let ticker: ((time: number) => void) | null = null;
|
||||||
|
|
||||||
// Disable lag smoothing to prevent jumps
|
void import('@studio-freight/lenis').then(({ default: Lenis }) => {
|
||||||
gsap.ticker.lagSmoothing(0);
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Reset scroll on route change
|
const lenis = new Lenis({
|
||||||
lenis.scrollTo(0, { immediate: true });
|
duration: 1.2,
|
||||||
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
|
direction: 'vertical',
|
||||||
|
gestureDirection: 'vertical',
|
||||||
|
smooth: true,
|
||||||
|
smoothTouch: false,
|
||||||
|
touchMultiplier: 2,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
lenisRef.current = lenis;
|
||||||
|
lenis.on('scroll', ScrollTrigger.update);
|
||||||
|
|
||||||
|
ticker = (time: number) => {
|
||||||
|
lenis.raf(time * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
gsap.ticker.add(ticker);
|
||||||
|
gsap.ticker.lagSmoothing(0);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
gsap.ticker.remove(ticker);
|
active = false;
|
||||||
lenis.destroy();
|
if (ticker) {
|
||||||
|
gsap.ticker.remove(ticker);
|
||||||
|
}
|
||||||
|
lenisRef.current?.destroy();
|
||||||
|
lenisRef.current = null;
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
lenisRef.current?.scrollTo(0, { immediate: true });
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,40 +95,55 @@ const AppContent: React.FC = () => {
|
|||||||
<GrainOverlay />
|
<GrainOverlay />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Suspense fallback={<RouteFallback />}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Routes>
|
||||||
<Route path="/about" element={<AboutPage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/services" element={<ServicesPage />} />
|
<Route path="/about" element={<AboutPage />} />
|
||||||
<Route path="/blog" element={<BlogPage />} />
|
<Route path="/services" element={<ServicesPage />} />
|
||||||
<Route path="/contact" element={<ContactPage />} />
|
<Route path="/blog" element={<BlogPage />} />
|
||||||
|
<Route path="/contact" element={<ContactPage />} />
|
||||||
|
<Route path="/locations" element={<LocationsPage />} />
|
||||||
|
<Route path="/privacy-policy" element={<PrivacyPolicyPage />} />
|
||||||
|
<Route path="/terms-of-service" element={<TermsOfServicePage />} />
|
||||||
|
|
||||||
{/* SEO Location Pages */}
|
{/* SEO Location Pages */}
|
||||||
{locationData.map((data) => (
|
{locationRoutes.map((data) => (
|
||||||
<Route
|
<React.Fragment key={data.slug}>
|
||||||
key={data.slug}
|
<Route
|
||||||
path={`/${data.slug}`}
|
path={data.path}
|
||||||
element={<LocationPage data={data} />}
|
element={<LocationSeoRoute slug={data.slug} />}
|
||||||
/>
|
/>
|
||||||
))}
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* SEO Service Pages */}
|
{/* SEO Service Pages */}
|
||||||
{serviceData.map((data) => (
|
{serviceRoutes.map((data) => (
|
||||||
<Route
|
<React.Fragment key={data.slug}>
|
||||||
key={data.slug}
|
<Route
|
||||||
path={`/${data.slug}`}
|
path={data.path}
|
||||||
element={<ServicePage data={data} />}
|
element={<ServiceSeoRoute slug={data.slug} />}
|
||||||
/>
|
/>
|
||||||
))}
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Authority Blog Posts */}
|
{/* Authority Blog Posts */}
|
||||||
{blogPostData.map((data) => (
|
{blogRoutes.map((data) => (
|
||||||
<Route
|
<React.Fragment key={data.slug}>
|
||||||
key={data.slug}
|
<Route
|
||||||
path={`/${data.slug}`}
|
path={data.path}
|
||||||
element={<BlogPostPage data={data} />}
|
element={<BlogSeoRoute slug={data.slug} />}
|
||||||
/>
|
/>
|
||||||
))}
|
</React.Fragment>
|
||||||
</Routes>
|
))}
|
||||||
|
|
||||||
|
{/* Legacy URL redirects — preserve old flat URLs */}
|
||||||
|
{legacyRedirects.map(({ from, to }) => (
|
||||||
|
<React.Fragment key={from}>
|
||||||
|
<Route path={from} element={<Navigate to={to} replace />} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
@@ -114,4 +157,4 @@ export default function App() {
|
|||||||
<AppContent />
|
<AppContent />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
22
Caddyfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Caddy inside the Docker container — listens on :80
|
||||||
|
# The host Caddy reverse-proxies to this container and handles HTTPS.
|
||||||
|
:80 {
|
||||||
|
root * /srv
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Content-Type-Options nosniff
|
||||||
|
X-Frame-Options DENY
|
||||||
|
Referrer-Policy strict-origin-when-cross-origin
|
||||||
|
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
# Long-term caching for hashed assets
|
||||||
|
@assets path /assets/* /images/* /logo.svg /public/*
|
||||||
|
header @assets Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
# SPA fallback: prerendered routes get their own index.html, rest falls back
|
||||||
|
try_files {path} {path}/index.html /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM caddy:2.8-alpine
|
||||||
|
|
||||||
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
|
COPY --from=build /app/dist /srv
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||||
298
README.md
@@ -1,53 +1,293 @@
|
|||||||
# Bay Area Affiliates
|
# Bay Area IT — Website
|
||||||
|
|
||||||
Ein modernes React-Projekt erstellt mit Vite, TypeScript und Framer Motion.
|
Marketing website for **Bay Area IT**, an IT service provider based in Corpus Christi, TX.
|
||||||
|
Built with React 19, Vite 6, TypeScript, and Tailwind CSS. Deployed as a static site served by Caddy inside Docker.
|
||||||
|
|
||||||
## Voraussetzungen
|
---
|
||||||
|
|
||||||
- Node.js (Version 18 oder höher empfohlen)
|
## Table of Contents
|
||||||
- npm (wird mit Node.js installiert)
|
|
||||||
|
|
||||||
## Installation
|
- [Tech Stack](#tech-stack)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Local Development](#local-development)
|
||||||
|
- [Build & Preview](#build--preview)
|
||||||
|
- [Deployment (Docker + Caddy)](#deployment-docker--caddy)
|
||||||
|
- [Adding Content](#adding-content)
|
||||||
|
- [Scripts Reference](#scripts-reference)
|
||||||
|
|
||||||
Klone das Repository und installiere die Abhängigkeiten:
|
---
|
||||||
|
|
||||||
```bash
|
## Tech Stack
|
||||||
npm install
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| UI Framework | React 19 |
|
||||||
|
| Build Tool | Vite 6 |
|
||||||
|
| Language | TypeScript 5 |
|
||||||
|
| Styling | Tailwind CSS 3 |
|
||||||
|
| Routing | React Router 7 |
|
||||||
|
| Animations | Framer Motion 12, GSAP 3 |
|
||||||
|
| Smooth Scroll | Lenis |
|
||||||
|
| Web Server | Caddy 2 (inside Docker) |
|
||||||
|
| Containerization | Docker + Docker Compose |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
bayarea/
|
||||||
|
├── components/ # Shared UI components
|
||||||
|
│ ├── Navbar.tsx
|
||||||
|
│ ├── Footer.tsx
|
||||||
|
│ ├── Hero.tsx
|
||||||
|
│ ├── Services.tsx
|
||||||
|
│ ├── Blog.tsx
|
||||||
|
│ ├── CTA.tsx
|
||||||
|
│ ├── Testimonials.tsx
|
||||||
|
│ ├── Process.tsx
|
||||||
|
│ ├── AreasWeServe.tsx
|
||||||
|
│ ├── SEO.tsx # Head/meta tag injection
|
||||||
|
│ ├── Breadcrumb.tsx
|
||||||
|
│ └── LoadingScreen.tsx
|
||||||
|
├── src/
|
||||||
|
│ ├── pages/ # Route-level page components
|
||||||
|
│ │ ├── HomePage.tsx
|
||||||
|
│ │ ├── AboutPage.tsx
|
||||||
|
│ │ ├── ServicesPage.tsx
|
||||||
|
│ │ ├── ServicePage.tsx # Dynamic: /services/:slug
|
||||||
|
│ │ ├── BlogPage.tsx
|
||||||
|
│ │ ├── BlogPostPage.tsx # Dynamic: /blog/:slug
|
||||||
|
│ │ ├── LocationPage.tsx # Dynamic: /locations/:slug
|
||||||
|
│ │ ├── LocationsPage.tsx
|
||||||
|
│ │ ├── ContactPage.tsx
|
||||||
|
│ │ ├── PrivacyPolicyPage.tsx
|
||||||
|
│ │ ├── TermsOfServicePage.tsx
|
||||||
|
│ │ └── LegalPage.tsx
|
||||||
|
│ ├── data/
|
||||||
|
│ │ └── seoData.ts # All location, service, and blog post data
|
||||||
|
│ ├── routes/ # Route definitions
|
||||||
|
│ └── index.css # Global styles
|
||||||
|
├── scripts/
|
||||||
|
│ ├── prerender-routes.ts # Generates per-route index.html files
|
||||||
|
│ ├── prune-dist-assets.mjs # Removes unused assets from dist/
|
||||||
|
│ ├── generate-sitemap.ts # Generates public/sitemap.xml
|
||||||
|
│ ├── generate-robots.ts # Generates public/robots.txt
|
||||||
|
│ └── optimize-images.mjs # Converts images to WebP
|
||||||
|
├── public/
|
||||||
|
│ ├── assets/services/ # Service page images (.webp)
|
||||||
|
│ ├── images/blog/ # Blog post images (.webp)
|
||||||
|
│ ├── sitemap.xml
|
||||||
|
│ └── robots.txt
|
||||||
|
├── Dockerfile # Multi-stage: Node build → Caddy serve
|
||||||
|
├── docker-compose.yml # Runs the container on localhost:8080
|
||||||
|
├── Caddyfile # Caddy config inside the container
|
||||||
|
├── caddy-host.snippet # Paste this into your host Caddy config
|
||||||
|
├── tailwind.config.cjs
|
||||||
|
├── postcss.config.cjs
|
||||||
|
├── vite.config.ts
|
||||||
|
└── tsconfig.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Entwicklung
|
---
|
||||||
|
|
||||||
Starte den lokalen Entwicklungsserver:
|
## Local Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 22+ (or use [nvm](https://github.com/nvm-sh/nvm))
|
||||||
|
- npm
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start the dev server
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Anwendung ist dann unter `http://localhost:5173` erreichbar.
|
The app runs at **http://localhost:3012**
|
||||||
|
|
||||||
## Build
|
Hot Module Replacement (HMR) is enabled. If port 3012 is taken, Vite picks the next available port automatically.
|
||||||
|
|
||||||
Erstelle eine optimierte Version für die Produktion:
|
---
|
||||||
|
|
||||||
|
## Build & Preview
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Production build (Vite + prerender + asset pruning)
|
||||||
npm run build
|
npm run build
|
||||||
```
|
|
||||||
|
|
||||||
Die erzeugten Dateien befinden sich im `dist`-Verzeichnis.
|
# Preview the production build locally
|
||||||
|
|
||||||
## Vorschau
|
|
||||||
|
|
||||||
Teste den Produktions-Build lokal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technologien
|
The build pipeline runs three steps in sequence:
|
||||||
|
|
||||||
- [React](https://react.dev/)
|
1. **`vite build`** — bundles and outputs to `dist/`
|
||||||
- [Vite](https://vitejs.dev/)
|
2. **`prune-dist-assets.mjs`** — removes any unreferenced files from `dist/assets/`
|
||||||
- [TypeScript](https://www.typescriptlang.org/)
|
3. **`prerender-routes.ts`** — writes a unique `index.html` per route into `dist/` with correct `<title>`, `<meta>`, canonical URLs, Open Graph tags, and JSON-LD schema
|
||||||
- [Framer Motion](https://www.framer.com/motion/) (Animationen)
|
|
||||||
- [GSAP](https://gsap.com/) (Animationen)
|
The result is a fully static site where every URL has its own HTML file — no server-side rendering required.
|
||||||
- [Lenis](https://github.com/studio-freight/lenis) (Smooth Scrolling)
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment (Docker + Caddy)
|
||||||
|
|
||||||
|
This project uses a **two-Caddy setup**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
└── Host Caddy (HTTPS, port 443)
|
||||||
|
└── Docker container (internal port 80 → host localhost:8080)
|
||||||
|
└── Caddy inside container
|
||||||
|
└── /srv (built static files)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1 — Build and start the container
|
||||||
|
|
||||||
|
On your server, copy the project files (or clone the repo), then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds the image (Node 22 compiles the app, Caddy serves it) and starts the container.
|
||||||
|
The app is now available at **http://localhost:8080** — only reachable from the server itself.
|
||||||
|
|
||||||
|
To verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I http://localhost:8080
|
||||||
|
# HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Configure the host Caddy
|
||||||
|
|
||||||
|
Add the following to your host Caddy config (usually `/etc/caddy/Caddyfile`).
|
||||||
|
A ready-to-paste version is in `caddy-host.snippet`:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
bayareait.services, www.bayareait.services {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
|
||||||
|
@www host www.bayareait.services
|
||||||
|
redir @www https://bayareait.services{uri} permanent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `bayareait.services` with your actual domain.
|
||||||
|
|
||||||
|
### Step 3 — Reload host Caddy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
# or
|
||||||
|
caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddy automatically obtains and renews an SSL certificate from Let's Encrypt — no manual certificate management needed.
|
||||||
|
|
||||||
|
### Updating the site
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest changes, rebuild, restart
|
||||||
|
git pull
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Zero-downtime: Docker Compose replaces the old container while the new one starts.
|
||||||
|
|
||||||
|
### Useful Docker commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Stop the container
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Rebuild without cache
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding Content
|
||||||
|
|
||||||
|
All page content is driven by **`src/data/seoData.ts`**. No new page files needed for standard locations, services, or blog posts — just add a new entry to the right array.
|
||||||
|
|
||||||
|
### Add a location page
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/data/seoData.ts → locationData array
|
||||||
|
{
|
||||||
|
city: "Sinton",
|
||||||
|
slug: "locations/it-support-sinton",
|
||||||
|
title: "IT Support Sinton, TX | Bay Area IT",
|
||||||
|
description: "...",
|
||||||
|
h1: "IT Support for Businesses in Sinton, TX",
|
||||||
|
keywords: ["IT support Sinton", "..."],
|
||||||
|
content: `<p>...</p>`,
|
||||||
|
faq: [
|
||||||
|
{ question: "...", answer: "..." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a service page
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/data/seoData.ts → serviceData array
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
slug: "services/cloud-backup",
|
||||||
|
title: "Cloud Backup Services | Bay Area IT",
|
||||||
|
description: "...",
|
||||||
|
h1: "Cloud Backup for Corpus Christi Businesses",
|
||||||
|
keywords: ["cloud backup", "..."],
|
||||||
|
content: `<p>...</p>`,
|
||||||
|
faq: []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a blog post
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/data/seoData.ts → blogPostData array
|
||||||
|
{
|
||||||
|
slug: "blog/your-post-slug",
|
||||||
|
title: "Your Post Title | Bay Area IT",
|
||||||
|
description: "...",
|
||||||
|
h1: "Your Post Heading",
|
||||||
|
keywords: ["..."],
|
||||||
|
content: `<p>...</p>`,
|
||||||
|
date: "2026-03-25",
|
||||||
|
image: "/images/blog/your-image.webp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After adding content, rebuild:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# or on the server:
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts Reference
|
||||||
|
|
||||||
|
| Script | Command | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Dev server | `npm run dev` | Starts Vite dev server on port 3012 |
|
||||||
|
| Production build | `npm run build` | Vite build + asset pruning + prerendering |
|
||||||
|
| Preview build | `npm run preview` | Serves `dist/` locally via Vite |
|
||||||
|
| Generate sitemap | `npm run generate:seo` | Writes `public/sitemap.xml` and `public/robots.txt` |
|
||||||
|
| Optimize images | `npm run optimize:images` | Converts images in `public/` to WebP |
|
||||||
|
| Prerender only | `npm run prerender:routes` | Re-runs route prerendering on existing `dist/` |
|
||||||
|
|||||||
12
caddy-host.snippet
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Paste this block into your HOST Caddy config (e.g. /etc/caddy/Caddyfile)
|
||||||
|
# Replace the domain and adjust the port if you changed it in docker-compose.yml
|
||||||
|
|
||||||
|
bayareait.services, www.bayareait.services {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
|
||||||
|
# Optional: redirect www → non-www
|
||||||
|
@www host www.bayareait.services
|
||||||
|
redir @www https://bayareait.services{uri} permanent
|
||||||
|
}
|
||||||
240
competitor-alternatives.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Competitor Alternatives Plan
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Product: Bay Area IT
|
||||||
|
- Category: Managed IT services for SMBs
|
||||||
|
- Market: Corpus Christi and the Coastal Bend
|
||||||
|
- Primary goal: Generate qualified local leads for managed IT and outsourced IT support
|
||||||
|
- Primary CTA: Book a free 20-minute IT assessment
|
||||||
|
|
||||||
|
## Positioning Summary
|
||||||
|
|
||||||
|
Bay Area IT should frame itself as the practical local alternative to reactive break-fix shops, generic remote-only MSPs, and the cost of hiring a full in-house IT person too early. The strongest angle is not "more features." It is clearer ownership, faster help, better day-to-day reliability, and local accountability for small and mid-sized businesses.
|
||||||
|
|
||||||
|
## Recommended Page Set
|
||||||
|
|
||||||
|
### Priority 1
|
||||||
|
|
||||||
|
1. `outsourced-it-support-vs-in-house-it.md`
|
||||||
|
2. `break-fix-it-support-alternative.md`
|
||||||
|
3. `managed-it-services-vs-freelance-it-support.md`
|
||||||
|
|
||||||
|
### Priority 2
|
||||||
|
|
||||||
|
1. `best-it-support-companies-corpus-christi.md`
|
||||||
|
2. `remote-it-support-vs-local-it-support.md`
|
||||||
|
3. `msp-vs-internal-it-manager.md`
|
||||||
|
|
||||||
|
### Priority 3
|
||||||
|
|
||||||
|
1. `bay-area-it-vs-national-it-provider.md`
|
||||||
|
2. `computer-repair-shop-vs-managed-it-services.md`
|
||||||
|
3. `best-managed-it-service-alternatives-coastal-bend.md`
|
||||||
|
|
||||||
|
## Why These Pages Matter
|
||||||
|
|
||||||
|
These pages match the real evaluation paths local SMB buyers use:
|
||||||
|
|
||||||
|
- They compare outsourced IT against hiring.
|
||||||
|
- They look for alternatives after frustration with reactive support.
|
||||||
|
- They compare local accountability against cheaper but fragmented options.
|
||||||
|
- They often search by problem first, not by a known competitor brand.
|
||||||
|
|
||||||
|
## Core Messaging Angles
|
||||||
|
|
||||||
|
### Bay Area IT is best for
|
||||||
|
|
||||||
|
- SMBs that want one accountable technology partner
|
||||||
|
- Teams losing time to recurring issues, slow machines, email problems, and network friction
|
||||||
|
- Businesses that need remote-first support with on-site help when needed
|
||||||
|
- Owners and office managers who are tired of acting as part-time IT
|
||||||
|
|
||||||
|
### Bay Area IT is not the best fit for
|
||||||
|
|
||||||
|
- Large enterprises with full internal IT departments
|
||||||
|
- Buyers who only want ad hoc hourly repair
|
||||||
|
- Organizations seeking the absolute lowest-cost option with no ongoing support relationship
|
||||||
|
|
||||||
|
## Common Switch Triggers
|
||||||
|
|
||||||
|
- Slow computers and outdated systems reduce productivity
|
||||||
|
- Support requests keep piling up without a clear owner
|
||||||
|
- Email, Wi-Fi, printer, and access issues interrupt normal work
|
||||||
|
- Current provider is reactive, vague, or hard to reach
|
||||||
|
- Internal staff are spending too much time on IT work
|
||||||
|
|
||||||
|
## Page Template 1: Break-Fix IT Support Alternative
|
||||||
|
|
||||||
|
### Suggested URL
|
||||||
|
|
||||||
|
`/break-fix-it-support-alternative`
|
||||||
|
|
||||||
|
### Title Tag
|
||||||
|
|
||||||
|
Break-Fix IT Support Alternative for Corpus Christi Businesses
|
||||||
|
|
||||||
|
### Meta Description
|
||||||
|
|
||||||
|
Looking for a better alternative to break-fix IT support? See how Bay Area IT helps Corpus Christi businesses reduce downtime with ongoing support, monitoring, and fast local help.
|
||||||
|
|
||||||
|
### H1
|
||||||
|
|
||||||
|
The Local Alternative to Break-Fix IT Support
|
||||||
|
|
||||||
|
### TL;DR
|
||||||
|
|
||||||
|
Break-fix support can be fine for isolated issues, but it usually leaves SMBs stuck in a cycle of recurring downtime, surprise costs, and no clear ownership. Bay Area IT is a better fit for businesses that want ongoing support, faster response, and a partner focused on keeping systems stable before problems escalate.
|
||||||
|
|
||||||
|
### Section Outline
|
||||||
|
|
||||||
|
#### Why businesses look for an alternative
|
||||||
|
|
||||||
|
Many small businesses start with break-fix support because it feels simpler and cheaper. The problem is that it only starts working after something is already broken. That means slow computers, email issues, weak Wi-Fi, and recurring employee tickets keep coming back, while managers keep losing time coordinating repairs.
|
||||||
|
|
||||||
|
#### Bay Area IT as the alternative
|
||||||
|
|
||||||
|
Bay Area IT provides ongoing support instead of isolated repair visits. That includes day-to-day help desk coverage, practical security baselines, device and user support, network help, and a local team that can step in remotely or on-site. The difference is not just fixing issues faster. It is reducing how often they happen in the first place.
|
||||||
|
|
||||||
|
#### Comparison snapshot
|
||||||
|
|
||||||
|
| Category | Bay Area IT | Break-Fix Provider |
|
||||||
|
|---|---|---|
|
||||||
|
| Support model | Ongoing monthly support | Reactive per-incident support |
|
||||||
|
| Ownership | One accountable partner | Usually ticket-by-ticket |
|
||||||
|
| Cost structure | Predictable recurring pricing | Variable and harder to forecast |
|
||||||
|
| Prevention | Monitoring and maintenance | Limited preventive work |
|
||||||
|
| Business fit | SMBs that want reliability | SMBs with very occasional issues |
|
||||||
|
|
||||||
|
#### Who should switch
|
||||||
|
|
||||||
|
Bay Area IT is the better choice if your team depends on email, cloud apps, printers, Wi-Fi, shared drives, and employee devices every day and cannot afford repeated disruption. Break-fix is still reasonable for very small environments that truly only need occasional repair.
|
||||||
|
|
||||||
|
#### CTA
|
||||||
|
|
||||||
|
Book a free 20-minute assessment and see whether ongoing IT support would reduce downtime for your team.
|
||||||
|
|
||||||
|
## Page Template 2: Outsourced IT Support vs In-House IT
|
||||||
|
|
||||||
|
### Suggested URL
|
||||||
|
|
||||||
|
`/outsourced-it-support-vs-in-house-it`
|
||||||
|
|
||||||
|
### Title Tag
|
||||||
|
|
||||||
|
Outsourced IT Support vs In-House IT for Small Businesses
|
||||||
|
|
||||||
|
### Meta Description
|
||||||
|
|
||||||
|
Comparing outsourced IT support to hiring in-house IT? See the tradeoffs in cost, coverage, speed, and business fit for Corpus Christi small businesses.
|
||||||
|
|
||||||
|
### H1
|
||||||
|
|
||||||
|
Outsourced IT Support vs In-House IT: Which Is Better for a Growing SMB?
|
||||||
|
|
||||||
|
### TL;DR
|
||||||
|
|
||||||
|
For many small and mid-sized businesses, outsourced IT is the more practical option because it provides broader coverage at a lower fixed cost than hiring a full-time internal employee too early. In-house IT becomes stronger when the business has enough scale, internal complexity, and daily demand to justify dedicated full-time ownership.
|
||||||
|
|
||||||
|
### Comparison Table
|
||||||
|
|
||||||
|
| Category | Bay Area IT / Outsourced IT | In-House IT |
|
||||||
|
|---|---|---|
|
||||||
|
| Cost | Usually lower and more predictable for SMBs | Higher salary, benefits, and hiring overhead |
|
||||||
|
| Coverage | Broader skill coverage across support areas | Depends on one person's strengths |
|
||||||
|
| Speed | Fast remote help with local escalation | Strong if fully staffed and available |
|
||||||
|
| Scalability | Easier to ramp without adding headcount | Requires hiring and management |
|
||||||
|
| Best for | SMBs without a mature IT department | Larger or more complex environments |
|
||||||
|
|
||||||
|
### Key Sections
|
||||||
|
|
||||||
|
#### Cost comparison
|
||||||
|
|
||||||
|
Most SMBs underestimate the full cost of internal IT. Salary is only the starting point. Hiring, benefits, downtime risk during turnover, and tool overhead all add up quickly. Outsourced support works best when a company needs coverage across many day-to-day IT functions but does not need a full internal team yet.
|
||||||
|
|
||||||
|
#### Breadth vs depth
|
||||||
|
|
||||||
|
An internal IT hire can be valuable, but one person rarely covers everything equally well across support, networking, email, vendors, security basics, onboarding, backups, and documentation. Bay Area IT should position breadth and consistency as a major advantage here.
|
||||||
|
|
||||||
|
#### Best-fit summary
|
||||||
|
|
||||||
|
Choose outsourced IT if you want dependable support without taking on full-time headcount. Choose in-house IT when your environment is large enough to justify dedicated internal ownership every day.
|
||||||
|
|
||||||
|
## Page Template 3: Managed IT Services vs Freelance IT Support
|
||||||
|
|
||||||
|
### Suggested URL
|
||||||
|
|
||||||
|
`/managed-it-services-vs-freelance-it-support`
|
||||||
|
|
||||||
|
### Title Tag
|
||||||
|
|
||||||
|
Managed IT Services vs Freelance IT Support
|
||||||
|
|
||||||
|
### Meta Description
|
||||||
|
|
||||||
|
Freelance IT support can work for occasional help, but managed IT services offer more structure, accountability, and continuity for growing small businesses.
|
||||||
|
|
||||||
|
### H1
|
||||||
|
|
||||||
|
Managed IT Services vs Freelance IT Support
|
||||||
|
|
||||||
|
### TL;DR
|
||||||
|
|
||||||
|
Freelance IT support can be affordable and flexible, but it often depends on one person's availability and process. Managed IT services are a better fit for businesses that want reliable response, recurring maintenance, clearer documentation, and less operational risk.
|
||||||
|
|
||||||
|
### Key Differentiators
|
||||||
|
|
||||||
|
- Process and coverage over one-person availability
|
||||||
|
- Ongoing support instead of ad hoc fixes
|
||||||
|
- Better continuity if one technician is unavailable
|
||||||
|
- Stronger fit for teams with multiple users and recurring support needs
|
||||||
|
|
||||||
|
## Reusable Proof Themes
|
||||||
|
|
||||||
|
- Local presence in Corpus Christi
|
||||||
|
- Regional service across the Coastal Bend
|
||||||
|
- Remote-first support with on-site help when needed
|
||||||
|
- Broad scope across help desk, email, networking, devices, backups, and security basics
|
||||||
|
- Low-friction CTA through a free 20-minute assessment
|
||||||
|
|
||||||
|
## Honest Weaknesses to Acknowledge
|
||||||
|
|
||||||
|
- Bay Area IT is not the lowest-cost option if a business only needs one-off repairs
|
||||||
|
- Businesses with complex enterprise compliance requirements may need a larger specialist provider
|
||||||
|
- No public customer-switching quotes are currently documented in the repo
|
||||||
|
|
||||||
|
## Content Gaps To Fill Before Publishing
|
||||||
|
|
||||||
|
1. Confirm the canonical brand name: Bay Area IT, Bay Area IT Services, or Bay Area Affiliates.
|
||||||
|
2. Normalize proof points that currently conflict in the repo, especially years in business and customer count.
|
||||||
|
3. Add at least 2 real switching testimonials.
|
||||||
|
4. Document migration/onboarding process for new managed IT clients.
|
||||||
|
5. Add concrete pricing guidance or example monthly plan ranges if the business is comfortable publishing them.
|
||||||
|
|
||||||
|
## Competitor Data Template
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
competitor:
|
||||||
|
name:
|
||||||
|
type:
|
||||||
|
geography:
|
||||||
|
target_audience:
|
||||||
|
pricing_model:
|
||||||
|
strengths: []
|
||||||
|
weaknesses: []
|
||||||
|
best_for: []
|
||||||
|
not_ideal_for: []
|
||||||
|
common_complaints: []
|
||||||
|
migration_notes: []
|
||||||
|
comparison_angle:
|
||||||
|
```
|
||||||
|
|
||||||
|
## CTA Options
|
||||||
|
|
||||||
|
- Book your free 20-minute IT assessment
|
||||||
|
- Talk to a local IT partner about your current setup
|
||||||
|
- See if managed IT support would reduce downtime for your team
|
||||||
|
|
||||||
|
## Recommended Next Step
|
||||||
|
|
||||||
|
Turn the first three priority pages into publishable landing-page copy, then add local proof and real switching testimonials before rollout.
|
||||||
@@ -3,6 +3,10 @@ import React from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { locationData } from '../src/data/seoData';
|
import { locationData } from '../src/data/seoData';
|
||||||
|
|
||||||
|
const cityLocations = locationData.filter(loc =>
|
||||||
|
loc.slug.startsWith('locations/it-support-')
|
||||||
|
);
|
||||||
|
|
||||||
const AreasWeServe: React.FC = () => {
|
const AreasWeServe: React.FC = () => {
|
||||||
return (
|
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">
|
<section className="py-24 px-6 bg-gray-50 dark:bg-white/5 mx-auto text-center border-t border-gray-200 dark:border-white/10">
|
||||||
@@ -12,11 +16,11 @@ const AreasWeServe: React.FC = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed">
|
<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.
|
We provide professional IT support and IT services for businesses throughout Corpus Christi and the surrounding Coastal Bend area.
|
||||||
Our team supports local companies with business IT support, outsourced IT services, and help desk solutions, delivered remotely or on-site when needed.
|
Our team supports local companies with outsourced IT services and help desk solutions, delivered remotely or on-site when needed.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-12">
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-12">
|
||||||
{locationData.map((loc) => (
|
{cityLocations.map((loc) => (
|
||||||
<Link
|
<Link
|
||||||
key={loc.slug}
|
key={loc.slug}
|
||||||
to={`/${loc.slug}`}
|
to={`/${loc.slug}`}
|
||||||
@@ -31,8 +35,8 @@ const AreasWeServe: React.FC = () => {
|
|||||||
Not sure if your location is covered? <Link to="/contact" className="underline hover:text-black dark:hover:text-white transition-colors">Contact us today</Link> to discuss your IT needs.
|
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>
|
</p>
|
||||||
<div className="mt-6">
|
<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">
|
<Link to="/locations" 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
|
View all service areas →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
|
|
||||||
const BackToTop: React.FC = () => {
|
const BackToTop: React.FC = () => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const toggleVisibility = () => {
|
const toggleVisibility = () => {
|
||||||
if (window.scrollY > 500) {
|
if (window.scrollY > 500) {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
} else {
|
} else {
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', toggleVisibility);
|
window.addEventListener('scroll', toggleVisibility);
|
||||||
return () => window.removeEventListener('scroll', toggleVisibility);
|
return () => window.removeEventListener('scroll', toggleVisibility);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
gsap.to(window, { duration: 1.2, scrollTo: 0, ease: "power3.inOut" });
|
gsap.to(window, { duration: 1.2, scrollTo: 0, ease: "power3.inOut" });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<motion.button
|
<motion.button
|
||||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||||
whileHover={{ scale: 1.1, backgroundColor: "#3b82f6" }}
|
whileHover={{ scale: 1.1, backgroundColor: "#3b82f6" }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
className="fixed bottom-8 right-8 z-50 w-12 h-12 flex items-center justify-center rounded-full bg-black dark:bg-white text-white dark:text-black shadow-lg border border-gray-700 dark:border-gray-200 transition-colors"
|
className="fixed bottom-8 right-8 z-50 w-12 h-12 flex items-center justify-center rounded-full bg-black dark:bg-white text-white dark:text-black shadow-lg border border-gray-700 dark:border-gray-200 transition-colors"
|
||||||
aria-label="Back to top"
|
aria-label="Back to top"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-2xl">arrow_upward</span>
|
<span className="material-symbols-outlined text-2xl">arrow_upward</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BackToTop;
|
export default BackToTop;
|
||||||
@@ -2,97 +2,86 @@ import React, { useRef, useLayoutEffect } from 'react';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
import { blogPostData } from '../src/data/seoData';
|
||||||
|
|
||||||
const posts = [
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
{
|
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuARalmRkuoZMBAbavGQgx4a-JhLgXBJ6JSD0U4vycdwaGGV3d-ffUFrdbx2lIbKrYCmS100i7VJ0w5cDHITXYV6w1-pSUPHKL7Jik__TWOIYOnq_4ND5ri7l8SQoaJdjJK9jhYvtxdxrZm6j8t8BNAjvPTaUdUDo4C7QVqcx1KbGvup6cpF8vY1LJ82S_5OMAZ6JgH0rK5bvWpqD3WqPhtqJCUB6d_1gUvluKjotwnNQ03t1dSYV8HOtRrLE83j6i_wgL4GZ0XTsMZb',
|
const posts = blogPostData
|
||||||
date: 'Jan 10, 2026',
|
.filter((post) => !post.redirect)
|
||||||
category: 'Performance',
|
.slice(0, 3)
|
||||||
title: 'Upgrade Your HDD to SSD for Enhanced Performance',
|
.map((post) => ({
|
||||||
excerpt: 'In today\'s fast-paced digital world, the performance of your computer can make a significant difference in productivity...'
|
image: post.image || '/images/blog/business-email-comparison-new.png',
|
||||||
},
|
category: post.category === 'authority' ? 'IT Insights' : 'Local Services',
|
||||||
{
|
title: post.h1,
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCz5lTYjY4RNXubQlrA-BtLIGR3nUY8ULkD9omwT5FShfdMrbMgS5dDCyfN3xiB5WC7T3vjNvyvVbvnD0G1zBpbNTjfOYyhmAEfno7Hf5W1sm-KYRXYrLGQq-c6TkLgEf0i9JGNvuFZ6edcenr2o39dCzIPXcp_z9XWOIzp7kBX2EydNPLJoRofVYuSTmEA1y0_xh4sdiRy1PykRASGLhKfN19_XLNuwyTBVKYISY7cHc-An69eZpAfhrvngu3E47rU6KuQS0k3QXBZ',
|
excerpt: post.description,
|
||||||
date: 'Jan 5, 2026',
|
href: `/${post.slug}`,
|
||||||
category: 'Security',
|
}));
|
||||||
title: 'Secure Your Corporate Network Access with WireGuard VPN',
|
|
||||||
excerpt: 'The safest way to access your corporate network remotely is through a secure VPN connection...'
|
const Blog: React.FC = () => {
|
||||||
},
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
{
|
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCl5iOhTsCqcHnho89DkoLh0DYeuvef0pdp8k26NKzcAq7YPvWbAYARg9mCIvqGTxQGradp8zvscuuibskpz4W_nEzQQO1z7lgwKJ1Xxiw_yQOyXMLfoRNLTHXzqFUH8Q5daCAfYTb7Zl3sFjB7k8i44D6TGolzqrN05Db27Abf2TWDDzHpVSrNml4zddvxholHFxMzqDeSzQ5p77SLDSFNaYBZGR2lEdN2V9O0GzMqxbOjFmBGMW48nlrEDLDzYGv_gWI3RSqNqBl-',
|
imagesRef.current = [];
|
||||||
date: 'Dec 28, 2025',
|
|
||||||
category: 'Infrastructure',
|
useLayoutEffect(() => {
|
||||||
title: 'Virtualizing Windows Machines: Future-Proof Your Corporate Network',
|
const ctx = gsap.context(() => {
|
||||||
excerpt: 'In October 2025, Microsoft will end support for Windows 10. Learn how virtualization can help you prepare...'
|
imagesRef.current.forEach((imgWrapper) => {
|
||||||
}
|
if (!imgWrapper) return;
|
||||||
];
|
|
||||||
|
gsap.to(imgWrapper, {
|
||||||
const Blog: React.FC = () => {
|
yPercent: 30,
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
ease: "none",
|
||||||
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
|
scrollTrigger: {
|
||||||
imagesRef.current = [];
|
trigger: imgWrapper.closest('article'),
|
||||||
|
start: "top bottom",
|
||||||
useLayoutEffect(() => {
|
end: "bottom top",
|
||||||
const ctx = gsap.context(() => {
|
scrub: true
|
||||||
imagesRef.current.forEach((imgWrapper) => {
|
}
|
||||||
if (!imgWrapper) return;
|
});
|
||||||
|
});
|
||||||
gsap.to(imgWrapper, {
|
}, containerRef);
|
||||||
yPercent: 30,
|
|
||||||
ease: "none",
|
return () => ctx.revert();
|
||||||
scrollTrigger: {
|
}, []);
|
||||||
trigger: imgWrapper.closest('article'),
|
|
||||||
start: "top bottom",
|
return (
|
||||||
end: "bottom top",
|
<motion.section
|
||||||
scrub: true
|
ref={containerRef}
|
||||||
}
|
id="blog"
|
||||||
});
|
initial={{ opacity: 0, y: 50 }}
|
||||||
});
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
}, containerRef);
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
return () => ctx.revert();
|
className="py-24 bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
||||||
}, []);
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
return (
|
<div className="flex justify-between items-end mb-12">
|
||||||
<motion.section
|
<div>
|
||||||
ref={containerRef}
|
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Latest Insights</span>
|
||||||
id="blog"
|
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
||||||
initial={{ opacity: 0, y: 50 }}
|
Stay updated <span className="text-gray-400 dark:text-gray-600">with our latest news and articles.</span>
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
</h2>
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
</div>
|
||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
<Link
|
||||||
className="py-24 bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
to="/blog"
|
||||||
>
|
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
|
||||||
<div className="flex justify-between items-end mb-12">
|
|
||||||
<div>
|
|
||||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Latest Insights</span>
|
|
||||||
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
|
||||||
Stay updated <span className="text-gray-400 dark:text-gray-600">with our latest news and articles.</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<motion.a
|
|
||||||
href="#"
|
|
||||||
className="hidden md:inline-flex items-center text-sm font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
className="hidden md:inline-flex items-center text-sm font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||||
whileHover={{ x: 5 }}
|
|
||||||
>
|
>
|
||||||
View all posts <span className="material-symbols-outlined text-sm ml-1">arrow_forward</span>
|
View all posts <span className="material-symbols-outlined text-sm ml-1">arrow_forward</span>
|
||||||
</motion.a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{posts.map((post, i) => (
|
{posts.map((post, i) => (
|
||||||
<motion.article
|
<motion.article
|
||||||
key={i}
|
key={i}
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: i * 0.1 }}
|
transition={{ duration: 0.5, delay: i * 0.1 }}
|
||||||
whileHover={{ y: -8 }}
|
whileHover={{ y: -8 }}
|
||||||
className="group cursor-pointer"
|
className="group"
|
||||||
>
|
>
|
||||||
|
<Link to={post.href} className="block">
|
||||||
<div className="h-64 rounded-xl overflow-hidden mb-6 relative shadow-lg">
|
<div className="h-64 rounded-xl overflow-hidden mb-6 relative shadow-lg">
|
||||||
<div
|
<div
|
||||||
ref={el => { if (el) imagesRef.current.push(el); }}
|
ref={el => { if (el) imagesRef.current.push(el); }}
|
||||||
@@ -101,17 +90,18 @@ const Blog: React.FC = () => {
|
|||||||
<img
|
<img
|
||||||
src={post.image}
|
src={post.image}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="low"
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors pointer-events-none"></div>
|
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors pointer-events-none"></div>
|
||||||
<div className="absolute top-4 right-4 bg-white/90 dark:bg-black/80 backdrop-blur text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wider z-10">
|
<div className="absolute top-4 right-4 bg-white/90 dark:bg-black/80 backdrop-blur text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wider z-10">
|
||||||
Read
|
Read
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mb-3">
|
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
<span>{post.date}</span>
|
|
||||||
<span className="w-1 h-1 rounded-full bg-gray-400"></span>
|
|
||||||
<span className="text-blue-600 dark:text-blue-400 font-medium">{post.category}</span>
|
<span className="text-blue-600 dark:text-blue-400 font-medium">{post.category}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||||
@@ -120,12 +110,13 @@ const Blog: React.FC = () => {
|
|||||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||||
{post.excerpt}
|
{post.excerpt}
|
||||||
</p>
|
</p>
|
||||||
|
</Link>
|
||||||
</motion.article>
|
</motion.article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Blog;
|
export default Blog;
|
||||||
|
|||||||
35
components/Breadcrumb.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
label: string;
|
||||||
|
to?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbProps {
|
||||||
|
items: BreadcrumbItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Breadcrumb: React.FC<BreadcrumbProps> = ({ items }) => {
|
||||||
|
return (
|
||||||
|
<nav aria-label="breadcrumb" className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
{i > 0 && <span className="text-gray-400 dark:text-gray-600">/</span>}
|
||||||
|
{item.to ? (
|
||||||
|
<Link
|
||||||
|
to={item.to}
|
||||||
|
className="hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">{item.label}</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Breadcrumb;
|
||||||
@@ -21,7 +21,7 @@ const CTA: React.FC = () => {
|
|||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed"
|
className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed"
|
||||||
>
|
>
|
||||||
Join 150+ Coastal Bend businesses that trust us with their technology. Get started with a free 20-minute assessment.
|
Join 30+ Coastal Bend businesses that trust us with their technology. Get started with a free 20-minute assessment.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -43,6 +43,12 @@ const CTA: React.FC = () => {
|
|||||||
>
|
>
|
||||||
Send a message
|
Send a message
|
||||||
</Link>
|
</Link>
|
||||||
|
<a
|
||||||
|
href="tel:+13617658400"
|
||||||
|
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors mt-2"
|
||||||
|
>
|
||||||
|
Or call us: (361) 765-8400
|
||||||
|
</a>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -1,113 +1,113 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
const Contact: React.FC = () => {
|
const Contact: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
id="contact"
|
id="contact"
|
||||||
initial={{ opacity: 0, y: 50 }}
|
initial={{ opacity: 0, y: 50 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5"
|
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5"
|
||||||
>
|
>
|
||||||
<div className="max-w-3xl mx-auto px-6">
|
<div className="max-w-3xl mx-auto px-6">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center mb-12"
|
className="text-center mb-12"
|
||||||
>
|
>
|
||||||
<h2 className="font-display text-4xl md:text-5xl font-medium mb-6 text-gray-900 dark:text-white">
|
<h2 className="font-display text-4xl md:text-5xl font-medium mb-6 text-gray-900 dark:text-white">
|
||||||
Get in Touch
|
Get in Touch
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
||||||
We're here to help you with all your IT needs.
|
We're here to help you with all your IT needs.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.form
|
<motion.form
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
whileInView={{ opacity: 1 }}
|
whileInView={{ opacity: 1 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name *</label>
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name *</label>
|
||||||
<motion.input
|
<motion.input
|
||||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Your Name"
|
placeholder="Your Name"
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email *</label>
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email *</label>
|
||||||
<motion.input
|
<motion.input
|
||||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
placeholder="Your Email"
|
placeholder="Your Email"
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone (optional)</label>
|
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone (optional)</label>
|
||||||
<motion.input
|
<motion.input
|
||||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
type="tel"
|
type="tel"
|
||||||
id="phone"
|
id="phone"
|
||||||
placeholder="Your Phone Number"
|
placeholder="Your Phone Number"
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="company" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Company (optional)</label>
|
<label htmlFor="company" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Company (optional)</label>
|
||||||
<motion.input
|
<motion.input
|
||||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
type="text"
|
type="text"
|
||||||
id="company"
|
id="company"
|
||||||
placeholder="Your Company Name"
|
placeholder="Your Company Name"
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message *</label>
|
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message *</label>
|
||||||
<motion.textarea
|
<motion.textarea
|
||||||
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
whileFocus={{ scale: 1.01, borderColor: "#3b82f6" }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
id="message"
|
id="message"
|
||||||
placeholder="Your Message"
|
placeholder="Your Message"
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all h-32 resize-none"
|
className="w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-400/20 focus:border-blue-500 outline-none transition-all h-32 resize-none"
|
||||||
></motion.textarea>
|
></motion.textarea>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<motion.button
|
<motion.button
|
||||||
type="submit"
|
type="submit"
|
||||||
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff", border: "none" }}
|
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff", border: "none" }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium transition-all duration-300 w-full md:w-auto shadow-lg"
|
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium transition-all duration-300 w-full md:w-auto shadow-lg"
|
||||||
>
|
>
|
||||||
Send Message
|
Send Message
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
</div>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Contact;
|
export default Contact;
|
||||||
@@ -1,68 +1,109 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10 pt-16 pb-8">
|
<footer className="bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10 pt-16 pb-8">
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-12 mb-16">
|
||||||
<div className="col-span-1 md:col-span-2">
|
<div className="col-span-1 md:col-span-2">
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<div className="flex items-center gap-2 mb-6">
|
||||||
<span className="material-symbols-outlined text-xl text-gray-900 dark:text-white">dns</span>
|
<span className="material-symbols-outlined text-xl text-gray-900 dark:text-white">dns</span>
|
||||||
<span className="font-display font-bold text-lg tracking-tight text-gray-900 dark:text-white">Bay Area Affiliates</span>
|
<span className="font-display font-bold text-lg tracking-tight text-gray-900 dark:text-white">Bay Area IT</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs mb-6">
|
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs mb-6">
|
||||||
Providing reliable IT services and solutions to the Coastal Bend community for over 25 years.
|
Providing reliable IT services and practical technology support to the Coastal Bend community for over 25 years.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-wrap gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{['X', 'in', 'fb'].map((social) => (
|
<a href="tel:+13617658400" className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
<motion.a
|
(361) 765-8400
|
||||||
key={social}
|
</a>
|
||||||
href="#"
|
<a href="mailto:info@bayareaaffiliates.com" className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
whileHover={{ y: -5, borderColor: "#ffffff", color: "#ffffff" }}
|
info@bayareaaffiliates.com
|
||||||
className="w-8 h-8 flex items-center justify-center rounded border border-gray-300 dark:border-white/20 text-gray-600 dark:text-gray-400 transition-colors"
|
</a>
|
||||||
>
|
|
||||||
<span className="text-xs font-bold">{social}</span>
|
|
||||||
</motion.a>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Navigation</h4>
|
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Services</h4>
|
||||||
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{['Services', 'Features', 'Blog', 'Contact'].map((item) => (
|
{[
|
||||||
<li key={item}>
|
{ label: 'IT Help Desk', to: '/services/it-help-desk' },
|
||||||
<motion.a
|
{ label: 'Computer Support', to: '/services/computer-support' },
|
||||||
href="#"
|
{ label: 'Business Email', to: '/services/business-email-services' },
|
||||||
whileHover={{ x: 5, color: "#ffffff" }}
|
{ label: 'Domain & DNS', to: '/services/domain-registration-dns-support' },
|
||||||
className="inline-block transition-colors"
|
{ label: 'Web Design', to: '/services/web-design-corpus-christi' },
|
||||||
>
|
].map((item) => (
|
||||||
{item}
|
<li key={item.label}>
|
||||||
</motion.a>
|
<motion.div whileHover={{ x: 5 }} className="inline-block">
|
||||||
|
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Contact</h4>
|
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Areas We Serve</h4>
|
||||||
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<li>support@bayareaaffiliates.com</li>
|
{[
|
||||||
|
{ label: 'Corpus Christi', to: '/locations/it-support-corpus-christi' },
|
||||||
|
{ label: 'Portland, TX', to: '/locations/it-support-portland-tx' },
|
||||||
|
{ label: 'Rockport, TX', to: '/locations/it-support-rockport-tx' },
|
||||||
|
{ label: 'Aransas Pass', to: '/locations/it-support-aransas-pass-tx' },
|
||||||
|
{ label: 'Kingsville', to: '/locations/it-support-kingsville-tx' },
|
||||||
|
].map((item) => (
|
||||||
|
<li key={item.label}>
|
||||||
|
<motion.div whileHover={{ x: 5 }} className="inline-block">
|
||||||
|
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Company</h4>
|
||||||
|
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{[
|
||||||
|
{ label: 'About', to: '/about' },
|
||||||
|
{ label: 'Blog', to: '/blog' },
|
||||||
|
{ label: 'Contact', to: '/contact' },
|
||||||
|
{ label: 'Privacy Policy', to: '/privacy-policy' },
|
||||||
|
{ label: 'Terms of Service', to: '/terms-of-service' },
|
||||||
|
].map((item) => (
|
||||||
|
<li key={item.label}>
|
||||||
|
<motion.div whileHover={{ x: 5 }} className="inline-block">
|
||||||
|
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li>
|
||||||
|
<a href="mailto:info@bayareaaffiliates.com" className="hover:text-gray-900 dark:hover:text-white transition-colors">info@bayareaaffiliates.com</a>
|
||||||
|
</li>
|
||||||
<li>(361) 765-8400</li>
|
<li>(361) 765-8400</li>
|
||||||
<li>1001 Blucher St, Corpus Christi, TX 78401</li>
|
|
||||||
<li><motion.a whileHover={{ x: 5, color: "#ffffff" }} href="#" className="inline-block transition-colors">FAQ</motion.a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 dark:border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="border-t border-gray-200 dark:border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-600">
|
<p className="text-xs text-gray-500 dark:text-gray-600">
|
||||||
© 2026 Bay Area Affiliates, Inc. All rights reserved.
|
© 2026 Bay Area IT. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-6">
|
<div className="flex flex-wrap items-center justify-center gap-4 text-xs text-gray-500 dark:text-gray-600">
|
||||||
<motion.a whileHover={{ color: "#ffffff" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Privacy Policy</motion.a>
|
<Link to="/privacy-policy" className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
<motion.a whileHover={{ color: "#ffffff" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Terms of Service</motion.a>
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
<Link to="/terms-of-service" className="transition-colors hover:text-gray-900 dark:hover:text-white">
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,4 +111,4 @@ const Footer: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
@@ -1,115 +1,149 @@
|
|||||||
import React, { useRef, useLayoutEffect } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
|
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
|
import heroBg from '../src/assets/hero-bg.webp';
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
const Hero: React.FC = () => {
|
const Hero: React.FC = () => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const mouseX = useMotionValue(0);
|
const mouseX = useMotionValue(0);
|
||||||
const mouseY = useMotionValue(0);
|
const mouseY = useMotionValue(0);
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
const [isInteractive, setIsInteractive] = useState(false);
|
||||||
|
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
|
||||||
|
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prefersReducedMotion || typeof window === 'undefined') {
|
||||||
|
setIsInteractive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
|
||||||
|
const updateState = () => setIsInteractive(mediaQuery.matches);
|
||||||
|
|
||||||
|
updateState();
|
||||||
|
|
||||||
|
if (typeof mediaQuery.addEventListener === 'function') {
|
||||||
|
mediaQuery.addEventListener('change', updateState);
|
||||||
|
return () => mediaQuery.removeEventListener('change', updateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addListener(updateState);
|
||||||
|
return () => mediaQuery.removeListener(updateState);
|
||||||
|
}, [prefersReducedMotion]);
|
||||||
|
|
||||||
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
||||||
|
if (!isInteractive) return;
|
||||||
const { left, top } = currentTarget.getBoundingClientRect();
|
const { left, top } = currentTarget.getBoundingClientRect();
|
||||||
mouseX.set(clientX - left);
|
mouseX.set(clientX - left);
|
||||||
mouseY.set(clientY - top + 75);
|
mouseY.set(clientY - top + 75);
|
||||||
};
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
if (!isInteractive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
// Parallax Background
|
// Parallax Background
|
||||||
gsap.to(parallaxWrapperRef.current, {
|
gsap.to(parallaxWrapperRef.current, {
|
||||||
yPercent: 30,
|
yPercent: 30,
|
||||||
ease: "none",
|
ease: "none",
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: containerRef.current,
|
trigger: containerRef.current,
|
||||||
start: "top top",
|
start: "top top",
|
||||||
end: "bottom top",
|
end: "bottom top",
|
||||||
scrub: true
|
scrub: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Text Stagger Animation
|
// Text Stagger Animation
|
||||||
gsap.fromTo(".hero-stagger",
|
gsap.fromTo(".hero-stagger",
|
||||||
{ y: 50, opacity: 0 },
|
{ y: 50, opacity: 0 },
|
||||||
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
|
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
|
||||||
);
|
);
|
||||||
}, containerRef);
|
}, containerRef);
|
||||||
|
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, []);
|
}, [isInteractive]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={isInteractive ? handleMouseMove : undefined}
|
||||||
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
|
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||||
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
||||||
{/* Base Layer - Slightly Brighter */}
|
{/* Base Layer - Slightly Brighter */}
|
||||||
<img
|
<img
|
||||||
alt="Abstract dark technology background"
|
alt="Abstract dark technology background"
|
||||||
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
||||||
src="/src/assets/hero-bg.png"
|
src={heroBg}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="high"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Highlight Layer - Only visible via mask */}
|
{isInteractive && (
|
||||||
<motion.img
|
<motion.img
|
||||||
style={{
|
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
|
||||||
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
|
alt=""
|
||||||
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
|
aria-hidden="true"
|
||||||
}}
|
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
||||||
alt=""
|
src={heroBg}
|
||||||
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
loading="lazy"
|
||||||
src="/src/assets/hero-bg.png"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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-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 className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10 text-center max-w-4xl px-6">
|
<div className="relative z-10 text-center max-w-4xl px-6">
|
||||||
<div className="hero-stagger flex items-center justify-center gap-2 mb-6">
|
<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="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
||||||
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">Established 1998</span>
|
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">Serving the Coastal Bend since 2000</span>
|
||||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="hero-stagger font-display text-5xl md:text-7xl lg:text-8xl font-medium tracking-tighter leading-[1.1] mb-8 text-gray-900 dark:text-white">
|
<h1 className="hero-stagger font-display text-5xl md:text-7xl lg:text-8xl font-medium tracking-tighter leading-[1.1] mb-8 text-gray-900 dark:text-white">
|
||||||
Reliable IT Services<br />
|
Reliable IT Services<br />
|
||||||
<span className="text-gray-500 dark:text-gray-500">for Over 25 Years</span>
|
<span className="text-gray-500 dark:text-gray-500">for Over 25 Years</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="hero-stagger text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-10 font-light leading-relaxed">
|
<p className="hero-stagger text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-10 font-light leading-relaxed">
|
||||||
Providing top-notch Computer & Networking solutions to the Coastal Bend community.
|
Local IT support, help desk coverage, networking, email, and practical technology support for Coastal Bend businesses.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
|
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#services"
|
href="#services"
|
||||||
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium"
|
className="px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium"
|
||||||
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
|
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
IT Services
|
IT Services
|
||||||
</motion.a>
|
</motion.a>
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#contact"
|
href="#contact"
|
||||||
className="px-8 py-3 bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-full font-medium"
|
className="px-8 py-3 bg-transparent border border-gray-300 dark:border-white/20 text-gray-900 dark:text-white rounded-full font-medium"
|
||||||
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.1)", borderColor: "#ffffff" }}
|
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.1)", borderColor: "#ffffff" }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
Get in Touch
|
Get in Touch
|
||||||
</motion.a>
|
</motion.a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Hero;
|
export default Hero;
|
||||||
|
|||||||
111
components/LoadingScreen.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import gsap from 'gsap';
|
||||||
|
|
||||||
|
interface LoadingScreenProps {
|
||||||
|
onComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingScreen: React.FC<LoadingScreenProps> = ({ onComplete }) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
const progressRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Hide scrolling on the body while loading
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
onComplete();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chars = textRef.current?.querySelectorAll('.char');
|
||||||
|
|
||||||
|
// 1. Initial state: progress bar width 0
|
||||||
|
gsap.set(progressRef.current, { scaleX: 0, transformOrigin: 'left center' });
|
||||||
|
if (chars) {
|
||||||
|
gsap.set(chars, { yPercent: 100, opacity: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Animate the progress bar and reveal text simultaneously
|
||||||
|
tl.to(progressRef.current, {
|
||||||
|
scaleX: 1,
|
||||||
|
duration: 1.5,
|
||||||
|
ease: 'power3.inOut',
|
||||||
|
})
|
||||||
|
.to(
|
||||||
|
chars || [],
|
||||||
|
{
|
||||||
|
yPercent: 0,
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.8,
|
||||||
|
stagger: 0.05,
|
||||||
|
ease: 'power4.out',
|
||||||
|
},
|
||||||
|
'<0.5' // Start this animation 0.5s into the progress bar loading
|
||||||
|
)
|
||||||
|
// 3. Keep it visible briefly
|
||||||
|
.to({}, { duration: 0.3 })
|
||||||
|
// 4. Slide out the individual characters upwards
|
||||||
|
.to(
|
||||||
|
chars || [],
|
||||||
|
{
|
||||||
|
yPercent: -100,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.5,
|
||||||
|
stagger: 0.02,
|
||||||
|
ease: 'power3.in',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 5. Fade out progress line
|
||||||
|
.to(progressRef.current, { opacity: 0, duration: 0.3 }, '<')
|
||||||
|
// 6. Slide the whole curtain up to reveal the homepage
|
||||||
|
.to(containerRef.current, {
|
||||||
|
yPercent: -100,
|
||||||
|
duration: 1,
|
||||||
|
ease: 'expo.inOut',
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tl.kill();
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [onComplete]);
|
||||||
|
|
||||||
|
const text = 'BAY AREA IT';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-surface-dark dark:bg-background-dark text-primary"
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden mb-8">
|
||||||
|
<h1
|
||||||
|
ref={textRef}
|
||||||
|
className="font-display text-4xl md:text-6xl font-bold tracking-widest flex overflow-hidden"
|
||||||
|
>
|
||||||
|
{text.split('').map((char, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={`char inline-block opacity-0 ${char === ' ' ? 'w-4' : ''}`}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sleek Minimalist Loading Line */}
|
||||||
|
<div className="w-48 md:w-64 h-[2px] bg-white/10 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={progressRef}
|
||||||
|
className="h-full w-full bg-primary origin-left scale-x-0"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingScreen;
|
||||||
@@ -1,63 +1,63 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import Counter from './Counter';
|
import Counter from './Counter';
|
||||||
|
|
||||||
const Mission: React.FC = () => {
|
const Mission: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
initial={{ opacity: 0, y: 50 }}
|
initial={{ opacity: 0, y: 50 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
className="py-24 bg-background-light dark:bg-background-dark relative bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
className="py-24 bg-background-light dark:bg-background-dark relative bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-start">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -200 }}
|
initial={{ opacity: 0, x: -200 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-4 text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500">
|
<div className="flex items-center gap-2 mb-4 text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500">
|
||||||
<span className="w-2 h-2 rounded-full bg-gray-400 dark:bg-gray-600"></span>
|
<span className="w-2 h-2 rounded-full bg-gray-400 dark:bg-gray-600"></span>
|
||||||
Our Mission
|
Our Mission
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-display text-4xl md:text-5xl font-medium mb-6 leading-tight text-gray-900 dark:text-white">
|
<h2 className="font-display text-4xl md:text-5xl font-medium mb-6 leading-tight text-gray-900 dark:text-white">
|
||||||
Harness invisible power <span className="text-gray-400 dark:text-gray-600">to operate faster, focus deeper, and scale effortlessly.</span>
|
Harness invisible power <span className="text-gray-400 dark:text-gray-600">to operate faster, focus deeper, and scale effortlessly.</span>
|
||||||
</h2>
|
</h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 200 }}
|
initial={{ opacity: 0, x: 200 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.8, delay: 0.4 }}
|
transition={{ duration: 0.8, delay: 0.4 }}
|
||||||
className="pt-4"
|
className="pt-4"
|
||||||
>
|
>
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-400 leading-relaxed mb-8">
|
<p className="text-lg text-gray-600 dark:text-gray-400 leading-relaxed mb-8">
|
||||||
Technology shouldn't be a hurdle; it should be the wind at your back. From seamless cloud migrations to robust cybersecurity, we handle the complexities so you can focus on what matters most: your business.
|
Technology shouldn't be a hurdle; it should be the wind at your back. From seamless cloud migrations to robust cybersecurity, we handle the complexities so you can focus on what matters most: your business.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-8 border-t border-gray-200 dark:border-white/10 pt-8">
|
<div className="grid grid-cols-2 gap-8 border-t border-gray-200 dark:border-white/10 pt-8">
|
||||||
<motion.div whileHover={{ scale: 1.05 }} className="cursor-default">
|
<motion.div whileHover={{ scale: 1.05 }} className="cursor-default">
|
||||||
<span className="block text-3xl font-display font-bold mb-2 text-gray-900 dark:text-white flex items-center">
|
<span className="block text-3xl font-display font-bold mb-2 text-gray-900 dark:text-white flex items-center">
|
||||||
<Counter value={99.9} />%
|
<Counter value={99.9} />%
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-500">Uptime Guarantee</span>
|
<span className="text-sm text-gray-500 dark:text-gray-500">Uptime Guarantee</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div whileHover={{ scale: 1.05 }} className="cursor-default">
|
<motion.div whileHover={{ scale: 1.05 }} className="cursor-default">
|
||||||
<span className="block text-3xl font-display font-bold mb-2 text-gray-900 dark:text-white flex items-center">
|
<span className="block text-3xl font-display font-bold mb-2 text-gray-900 dark:text-white flex items-center">
|
||||||
<Counter value={24} />/7
|
<Counter value={24} />/7
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-500">Support Availability</span>
|
<span className="text-sm text-gray-500 dark:text-gray-500">Support Availability</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Mission;
|
export default Mission;
|
||||||
@@ -1,49 +1,48 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
const Navbar: React.FC = () => {
|
||||||
const Navbar: React.FC = () => {
|
const location = useLocation();
|
||||||
const location = useLocation();
|
const isHome = location.pathname === '/';
|
||||||
const isHome = location.pathname === '/';
|
|
||||||
|
return (
|
||||||
return (
|
<nav
|
||||||
<nav
|
className="fixed w-full z-40 top-0 left-0 border-b border-gray-200 dark:border-white/10 bg-white/80 dark:bg-background-dark/80 backdrop-blur-md"
|
||||||
className="fixed w-full z-40 top-0 left-0 border-b border-gray-200 dark:border-white/10 bg-white/80 dark:bg-background-dark/80 backdrop-blur-md"
|
>
|
||||||
>
|
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
<Link to="/" className="flex items-center gap-2">
|
||||||
<Link to="/" className="flex items-center gap-2">
|
<img
|
||||||
<motion.div
|
src="/logo.svg"
|
||||||
whileHover={{ rotate: 180 }}
|
alt="Bay Area IT logo"
|
||||||
transition={{ duration: 0.5 }}
|
className="h-8 w-8"
|
||||||
>
|
/>
|
||||||
<span className="material-symbols-outlined text-xl dark:text-white text-black">dns</span>
|
<span className="font-display font-bold text-lg tracking-tight">Bay Area IT</span>
|
||||||
</motion.div>
|
</Link>
|
||||||
<span className="font-display font-bold text-lg tracking-tight">Bay Area Affiliates</span>
|
|
||||||
</Link>
|
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{['About', 'Services', 'Blog', 'Contact'].map((item) => (
|
||||||
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-gray-600 dark:text-gray-400">
|
<Link
|
||||||
{['About', 'Services', 'Blog', 'Contact'].map((item) => (
|
key={item}
|
||||||
<Link
|
to={`/${item.toLowerCase()}`}
|
||||||
key={item}
|
className="hover:text-black dark:hover:text-white transition-colors relative group px-2 py-1"
|
||||||
to={`/${item.toLowerCase()}`}
|
>
|
||||||
className="hover:text-black dark:hover:text-white transition-colors relative group px-2 py-1"
|
<span className="inline-block">
|
||||||
>
|
{item}
|
||||||
<motion.span
|
</span>
|
||||||
whileHover={{ scale: 1.05 }}
|
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-black dark:bg-white transition-all duration-300 ease-out group-hover:w-full"></span>
|
||||||
whileTap={{ scale: 0.95 }}
|
</Link>
|
||||||
className="inline-block"
|
))}
|
||||||
>
|
</div>
|
||||||
{item}
|
|
||||||
</motion.span>
|
<Link
|
||||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-black dark:bg-white transition-all duration-300 ease-out group-hover:w-full"></span>
|
to="/contact"
|
||||||
</Link>
|
className="hidden md:inline-flex items-center px-4 py-1.5 bg-black dark:bg-white text-white dark:text-black rounded-full text-sm font-medium hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors"
|
||||||
))}
|
>
|
||||||
</div>
|
Get IT Support
|
||||||
|
</Link>
|
||||||
{/* Client Portal button removed */}
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</nav>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
export default Navbar;
|
||||||
export default Navbar;
|
|
||||||
|
|||||||
@@ -1,119 +1,149 @@
|
|||||||
import React, { useLayoutEffect, useRef } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, useReducedMotion } from 'framer-motion';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
|
import processIllustration from '../src/assets/process-illustration.webp';
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
const Process: React.FC = () => {
|
const Process: React.FC = () => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
const [supportsFinePointer, setSupportsFinePointer] = useState(false);
|
||||||
|
const shouldAnimate = !prefersReducedMotion && supportsFinePointer;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
|
||||||
|
const updateState = () => setSupportsFinePointer(mediaQuery.matches);
|
||||||
|
|
||||||
|
updateState();
|
||||||
|
|
||||||
|
if (typeof mediaQuery.addEventListener === 'function') {
|
||||||
|
mediaQuery.addEventListener('change', updateState);
|
||||||
|
return () => mediaQuery.removeEventListener('change', updateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addListener(updateState);
|
||||||
|
return () => mediaQuery.removeListener(updateState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
if (!shouldAnimate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = gsap.context((self) => {
|
const ctx = gsap.context((self) => {
|
||||||
// Dramatic Zoom Animation
|
// Dramatic Zoom Animation
|
||||||
if (containerRef.current && imgRef.current) {
|
if (containerRef.current && imgRef.current) {
|
||||||
gsap.fromTo(imgRef.current,
|
gsap.fromTo(imgRef.current,
|
||||||
{ scale: 1, transformOrigin: 'center center' },
|
{ scale: 1, transformOrigin: 'center center' },
|
||||||
{
|
{
|
||||||
scale: 2.0,
|
scale: 2.0,
|
||||||
ease: "none",
|
ease: "none",
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: containerRef.current,
|
trigger: containerRef.current,
|
||||||
start: "top bottom",
|
start: "top bottom",
|
||||||
end: "bottom top",
|
end: "bottom top",
|
||||||
scrub: true,
|
scrub: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate steps - even slower, one by one appearance
|
// Animate steps - even slower, one by one appearance
|
||||||
const steps = gsap.utils.selector(containerRef.current)('.process-step');
|
const steps = gsap.utils.selector(containerRef.current)('.process-step');
|
||||||
steps.forEach((step: any, index: number) => {
|
steps.forEach((step: any, index: number) => {
|
||||||
gsap.fromTo(step,
|
gsap.fromTo(step,
|
||||||
{ opacity: 0, y: 60 },
|
{ opacity: 0, y: 60 },
|
||||||
{
|
{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
duration: 2,
|
duration: 2,
|
||||||
ease: "power3.out",
|
ease: "power3.out",
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: step,
|
trigger: step,
|
||||||
start: "top 95%",
|
start: "top 95%",
|
||||||
end: "top 40%",
|
end: "top 40%",
|
||||||
toggleActions: "play reverse play reverse",
|
toggleActions: "play reverse play reverse",
|
||||||
scrub: 1.5
|
scrub: 1.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
}, containerRef);
|
}, containerRef);
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, []);
|
}, [shouldAnimate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section ref={containerRef} className="relative w-full" style={{ clipPath: 'inset(0)' }}>
|
<section ref={containerRef} className="relative w-full" style={{ clipPath: 'inset(0)' }}>
|
||||||
{/* Fixed Background Image - constrained to this section via clip-path */}
|
{/* Fixed Background Image - constrained to this section via clip-path */}
|
||||||
<div className="fixed inset-0 w-full h-screen z-0 overflow-hidden">
|
<div className={`${shouldAnimate ? 'fixed inset-0 w-full h-screen' : 'absolute inset-0 w-full h-full'} z-0 overflow-hidden`}>
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
alt="Modern server rack infrastructure"
|
alt="Modern server rack infrastructure"
|
||||||
className="w-full h-full object-cover opacity-80 will-change-transform origin-center"
|
className={`w-full h-full object-cover opacity-80 origin-center ${shouldAnimate ? 'will-change-transform' : ''}`}
|
||||||
src="/src/assets/process-illustration.png"
|
src={processIllustration}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="low"
|
||||||
/>
|
/>
|
||||||
{/* Gradient overlay for text readability */}
|
{/* Gradient overlay for text readability */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-black/50 via-black/30 to-black/60" />
|
<div className="absolute inset-0 bg-gradient-to-r from-black/50 via-black/30 to-black/60" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content - positioned relative, scrolls over the fixed image */}
|
{/* Content - positioned relative, scrolls over the fixed image */}
|
||||||
<div className="relative z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))]">
|
<div className="relative z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))]">
|
||||||
{/* Header - Static on mobile, fixed on desktop */}
|
{/* Header - Static on mobile, fixed on desktop */}
|
||||||
<div className="relative mb-12 lg:mb-0 lg:fixed lg:top-1/2 lg:right-16 lg:-translate-y-1/2 z-20 text-left lg:text-right px-6 lg:px-0" style={{ clipPath: 'none' }}>
|
<div className="relative mb-12 lg:mb-0 lg:fixed lg:top-1/2 lg:right-16 lg:-translate-y-1/2 z-20 text-left lg:text-right px-6 lg:px-0" style={{ clipPath: 'none' }}>
|
||||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-300 mb-2 block">Process</span>
|
<span className="text-xs font-semibold uppercase tracking-widest text-gray-300 mb-2 block">Process</span>
|
||||||
<h2 className="font-display text-3xl lg:text-5xl font-medium text-white">
|
<h2 className="font-display text-3xl lg:text-5xl font-medium text-white">
|
||||||
One consultation to begin,<br />
|
One consultation to begin,<br />
|
||||||
<span className="text-gray-400">three steps to clarity.</span>
|
<span className="text-gray-400">three steps to clarity.</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spacer for first screen - shortened */}
|
{/* Spacer for first screen - shortened */}
|
||||||
<div className="h-[30vh]" />
|
<div className="h-[30vh]" />
|
||||||
|
|
||||||
{/* Steps - LEFT side on desktop, full width on mobile */}
|
{/* Steps - LEFT side on desktop, full width on mobile */}
|
||||||
<div className="min-h-screen px-6 lg:px-16 py-24">
|
<div className="min-h-screen px-6 lg:px-16 py-24">
|
||||||
<div className="w-full lg:w-1/2 space-y-[60vh]">
|
<div className="w-full lg:w-1/2 space-y-[60vh]">
|
||||||
{[
|
{[
|
||||||
{ num: "1", title: "Audit & Assess", desc: "We dive deep into your current infrastructure to identify vulnerabilities and opportunities for optimization." },
|
{ num: "1", title: "Audit & Assess", desc: "We dive deep into your current infrastructure to identify vulnerabilities and opportunities for optimization." },
|
||||||
{ num: "2", title: "Implement & Secure", desc: "Our team deploys the necessary hardware and software solutions with minimal disruption to your daily operations." },
|
{ num: "2", title: "Implement & Secure", desc: "Our team deploys the necessary hardware and software solutions with minimal disruption to your daily operations." },
|
||||||
{ num: "3", title: "Monitor & Maintain", desc: "Ongoing 24/7 monitoring ensures problems are solved before you even notice them." }
|
{ num: "3", title: "Monitor & Maintain", desc: "Ongoing 24/7 monitoring ensures problems are solved before you even notice them." }
|
||||||
].map((step, i) => (
|
].map((step, i) => (
|
||||||
<div key={i} className="process-step flex gap-6 group cursor-default bg-black/60 backdrop-blur-md p-8 rounded-2xl border border-white/10">
|
<div key={i} className="process-step flex gap-6 group cursor-default bg-black/60 backdrop-blur-md p-8 rounded-2xl border border-white/10">
|
||||||
<div className="flex-shrink-0 mt-1">
|
<div className="flex-shrink-0 mt-1">
|
||||||
<motion.span
|
<motion.span
|
||||||
whileHover={{ scale: 1.2, borderColor: "#3b82f6", color: "#3b82f6" }}
|
whileHover={{ scale: 1.2, borderColor: "#3b82f6", color: "#3b82f6" }}
|
||||||
className="flex items-center justify-center w-12 h-12 rounded-xl border-2 border-white/30 text-lg font-bold text-white transition-colors"
|
className="flex items-center justify-center w-12 h-12 rounded-xl border-2 border-white/30 text-lg font-bold text-white transition-colors"
|
||||||
>
|
>
|
||||||
{step.num}
|
{step.num}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl lg:text-3xl font-medium text-white group-hover:translate-x-1 transition-transform group-hover:text-blue-400">{step.title}</h3>
|
<h3 className="text-2xl lg:text-3xl font-medium text-white group-hover:translate-x-1 transition-transform group-hover:text-blue-400">{step.title}</h3>
|
||||||
<p className="text-lg text-gray-300 mt-3 leading-relaxed max-w-lg">
|
<p className="text-lg text-gray-300 mt-3 leading-relaxed max-w-lg">
|
||||||
{step.desc}
|
{step.desc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* End spacer - shortened */}
|
{/* End spacer - shortened */}
|
||||||
<div className="h-[20vh]" />
|
<div className="h-[20vh]" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Process;
|
export default Process;
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ interface SEOProps {
|
|||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
canonicalUrl?: string;
|
canonicalUrl?: string;
|
||||||
schema?: object; // JSON-LD schema
|
schema?: object; // JSON-LD schema
|
||||||
|
ogImage?: string;
|
||||||
|
ogType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema }) => {
|
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema, ogImage, ogType }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Update Title
|
// Update Title
|
||||||
document.title = title;
|
document.title = title;
|
||||||
|
|
||||||
// Helper to set meta tag
|
// Helper to set meta tag (name attribute)
|
||||||
const setMetaTag = (name: string, content: string) => {
|
const setMetaTag = (name: string, content: string) => {
|
||||||
let element = document.querySelector(`meta[name="${name}"]`);
|
let element = document.querySelector(`meta[name="${name}"]`);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
@@ -25,6 +27,17 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
|
|||||||
element.setAttribute('content', content);
|
element.setAttribute('content', content);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to set OG tag (property attribute)
|
||||||
|
const setOgTag = (property: string, content: string) => {
|
||||||
|
let element = document.querySelector(`meta[property="${property}"]`);
|
||||||
|
if (!element) {
|
||||||
|
element = document.createElement('meta');
|
||||||
|
element.setAttribute('property', property);
|
||||||
|
document.head.appendChild(element);
|
||||||
|
}
|
||||||
|
element.setAttribute('content', content);
|
||||||
|
};
|
||||||
|
|
||||||
// Update Meta Description
|
// Update Meta Description
|
||||||
setMetaTag('description', description);
|
setMetaTag('description', description);
|
||||||
|
|
||||||
@@ -44,6 +57,19 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
|
|||||||
link.setAttribute('href', canonicalUrl);
|
link.setAttribute('href', canonicalUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open Graph Tags
|
||||||
|
setOgTag('og:title', title);
|
||||||
|
setOgTag('og:description', description);
|
||||||
|
setOgTag('og:type', ogType || 'website');
|
||||||
|
setOgTag('og:site_name', 'Bay Area IT');
|
||||||
|
if (canonicalUrl) setOgTag('og:url', canonicalUrl);
|
||||||
|
if (ogImage) setOgTag('og:image', ogImage);
|
||||||
|
|
||||||
|
// Twitter Card Tags
|
||||||
|
setMetaTag('twitter:card', 'summary');
|
||||||
|
setMetaTag('twitter:title', title);
|
||||||
|
setMetaTag('twitter:description', description);
|
||||||
|
|
||||||
// Inject Schema
|
// Inject Schema
|
||||||
if (schema) {
|
if (schema) {
|
||||||
const scriptId = 'seo-schema-script';
|
const scriptId = 'seo-schema-script';
|
||||||
@@ -57,10 +83,7 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
|
|||||||
script.textContent = JSON.stringify(schema);
|
script.textContent = JSON.stringify(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup function not strictly necessary for single page app navigation
|
}, [title, description, keywords, canonicalUrl, schema, ogImage, ogType]);
|
||||||
// unless we want to remove specific tags on unmount, but usually we just overwrite them.
|
|
||||||
|
|
||||||
}, [title, description, keywords, canonicalUrl, schema]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,243 +2,222 @@ 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';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
const servicesData = [
|
const servicesData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'IT Infrastructure',
|
category: 'Web Services',
|
||||||
title: 'Windows 11 Transition',
|
title: 'Web Design',
|
||||||
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
|
description: 'Professional websites with domain registration and DNS support to give your business a clean, reliable online presence.',
|
||||||
icon: 'desktop_windows',
|
icon: 'language',
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
|
image: '/assets/services/business-it.webp',
|
||||||
|
href: '/services/web-design-corpus-christi'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
category: 'Web Services',
|
category: 'Web Services',
|
||||||
title: 'Web Services',
|
title: 'Bay Area Email Services',
|
||||||
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
|
description: 'Enterprise cloud email with 99.99% uptime, local Texas support, 25 GB mailboxes, and business-grade delivery for $5 per inbox.',
|
||||||
icon: 'language',
|
icon: 'mail',
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
|
image: '/assets/services/business-it.webp',
|
||||||
|
href: '/services/business-email-services'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
category: 'IT Infrastructure',
|
category: 'IT Infrastructure',
|
||||||
title: 'Performance Upgrades',
|
title: 'Printer & Scanner Installation',
|
||||||
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
|
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
|
||||||
icon: 'speed',
|
icon: 'print',
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
|
image: '/assets/services/printer-scanner.webp',
|
||||||
|
href: '/services'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
category: 'IT Infrastructure',
|
category: 'IT Infrastructure',
|
||||||
title: 'Printer & Scanner Installation',
|
|
||||||
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
|
|
||||||
icon: 'print',
|
|
||||||
image: '/assets/services/printer-scanner.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
category: 'IT Infrastructure',
|
|
||||||
title: 'New/Refurbished Desktop Hardware',
|
title: 'New/Refurbished Desktop Hardware',
|
||||||
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
|
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
|
||||||
icon: 'computer',
|
icon: 'computer',
|
||||||
image: '/assets/services/desktop-hardware.png'
|
image: '/assets/services/desktop-hardware.webp',
|
||||||
|
href: '/services/computer-support'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 5,
|
||||||
category: 'Security',
|
|
||||||
title: 'VPN Setup',
|
|
||||||
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
|
|
||||||
icon: 'vpn_lock',
|
|
||||||
image: '/assets/services/vpn-setup.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
category: 'Networking',
|
category: 'Networking',
|
||||||
title: 'Network Infrastructure Support',
|
title: 'Network Infrastructure Support',
|
||||||
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
|
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
|
||||||
icon: 'lan',
|
icon: 'lan',
|
||||||
image: '/assets/services/network-infrastructure.png'
|
image: '/assets/services/network-infrastructure.webp',
|
||||||
|
href: '/services'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 6,
|
||||||
category: 'Networking',
|
category: 'Networking',
|
||||||
title: 'Network Attached Storage',
|
title: 'Shared Drive',
|
||||||
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
|
description: 'Setup and management of shared drive solutions so your team can store, access, and organize files reliably.',
|
||||||
icon: 'storage',
|
icon: 'storage',
|
||||||
image: '/assets/services/nas-storage.png'
|
image: '/assets/services/nas-storage.webp',
|
||||||
|
href: '/services'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 7,
|
||||||
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',
|
category: 'IT Infrastructure',
|
||||||
title: 'IT Help Desk',
|
title: 'IT Help Desk',
|
||||||
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
|
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
|
||||||
icon: 'support_agent',
|
icon: 'support_agent',
|
||||||
image: '/assets/services/help-desk.png'
|
image: '/assets/services/help-desk.webp',
|
||||||
},
|
href: '/services/it-help-desk'
|
||||||
{
|
|
||||||
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', 'Networking'];
|
||||||
|
|
||||||
interface ServicesProps {
|
interface ServicesProps {
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
featuredIds?: number[];
|
featuredIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
|
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
|
||||||
const [activeCategory, setActiveCategory] = useState('All');
|
const [activeCategory, setActiveCategory] = useState('All');
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Determine if we should be in "preview mode" (showing only a subset)
|
// 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"
|
// This applies if preview is true OR if featuredIds are provided and we haven't clicked "Show More"
|
||||||
const isRestrictedView = (preview || featuredIds) && !showAll;
|
const isRestrictedView = (preview || featuredIds) && !showAll;
|
||||||
|
|
||||||
// Filter services based on category first (unless in restricted view with specific IDs, where we might want to ignore category or just show the specific ones)
|
// 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'
|
const filteredByCategory = activeCategory === 'All'
|
||||||
? servicesData
|
? servicesData
|
||||||
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
|
: servicesData.filter(s => s.category === activeCategory);
|
||||||
|
|
||||||
const displayedServices = useMemo(() => {
|
const displayedServices = useMemo(() => {
|
||||||
if (isRestrictedView) {
|
if (isRestrictedView) {
|
||||||
if (featuredIds && featuredIds.length > 0) {
|
if (featuredIds && featuredIds.length > 0) {
|
||||||
// Sort the services to match the order of featuredIds
|
// Sort the services to match the order of featuredIds
|
||||||
return featuredIds
|
return featuredIds
|
||||||
.map(id => servicesData.find(s => s.id === id))
|
.map(id => servicesData.find(s => s.id === id))
|
||||||
.filter((s): s is typeof servicesData[0] => s !== undefined);
|
.filter((s): s is typeof servicesData[0] => s !== undefined);
|
||||||
}
|
}
|
||||||
// Fallback to first 3 if no IDs but preview is true
|
// Fallback to first 3 if no IDs but preview is true
|
||||||
return servicesData.slice(0, 3);
|
return servicesData.slice(0, 3);
|
||||||
}
|
}
|
||||||
// Show all (filtered by category)
|
// Show all (filtered by category)
|
||||||
return filteredByCategory;
|
return filteredByCategory;
|
||||||
}, [isRestrictedView, featuredIds, filteredByCategory]);
|
}, [isRestrictedView, featuredIds, filteredByCategory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
id="services"
|
id="services"
|
||||||
initial={{ opacity: 0, y: 50 }}
|
initial={{ opacity: 0, y: 50 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
className="py-24 bg-white dark:bg-[#0f0f0f] border-t border-gray-100 dark:border-white/5 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Our Services</span>
|
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-2 block">Our Services</span>
|
||||||
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
<h2 className="font-display text-3xl md:text-4xl text-gray-900 dark:text-white">
|
||||||
Comprehensive IT solutions <span className="text-gray-400 dark:text-gray-600">tailored to your business needs.</span>
|
Comprehensive IT solutions <span className="text-gray-400 dark:text-gray-600">tailored to your business needs.</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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. */}
|
{/* 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 && (
|
{!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">
|
<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) => (
|
{categories.map((cat) => (
|
||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setActiveCategory(cat)}
|
onClick={() => setActiveCategory(cat)}
|
||||||
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
|
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
|
||||||
? 'text-gray-900 dark:text-white'
|
? 'text-gray-900 dark:text-white'
|
||||||
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cat}
|
{cat}
|
||||||
{activeCategory === cat && (
|
{activeCategory === cat && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="activeTab"
|
layoutId="activeTab"
|
||||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
||||||
{displayedServices.map((service) => (
|
{displayedServices.map((service, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={service.id}
|
key={service.id}
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
whileHover={{ y: -10, transition: { duration: 0.3 } }}
|
whileHover={{ y: -10, transition: { duration: 0.3 } }}
|
||||||
className="group relative bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden hover:border-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300"
|
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-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
<div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
|
||||||
<img
|
<img
|
||||||
src={service.image}
|
src={service.image}
|
||||||
alt={service.title}
|
alt={service.title}
|
||||||
|
loading={isRestrictedView ? 'lazy' : index < 3 ? 'eager' : 'lazy'}
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority={isRestrictedView ? 'low' : index < 3 ? 'high' : 'low'}
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
|
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 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-4 relative">
|
<div className="p-4 relative">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-8 h-8 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-3 border border-gray-200 dark:border-white/10"
|
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-lg 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-3">
|
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
<a href="/services" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
|
<Link to={service.href} className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
|
||||||
Learn More <motion.span
|
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] }}
|
||||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", repeatDelay: 1 }}
|
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", repeatDelay: 1 }}
|
||||||
>arrow_forward</motion.span>
|
>arrow_forward</motion.span>
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isRestrictedView && (
|
{isRestrictedView && (
|
||||||
<div className="mt-12 text-center">
|
<div className="mt-12 text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAll(true)}
|
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"
|
||||||
>
|
>
|
||||||
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
|
Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
|
||||||
</button>
|
</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". */}
|
{/* 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Services;
|
export default Services;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const Testimonials: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<blockquote className="text-xl md:text-2xl font-medium leading-relaxed text-gray-900 dark:text-white mb-8 relative z-10">
|
<blockquote className="text-xl md:text-2xl font-medium leading-relaxed text-gray-900 dark:text-white mb-8 relative z-10">
|
||||||
"Bay Area Affiliates transformed our IT infrastructure completely. Their proactive approach means we rarely have downtime, and when issues do arise, they're resolved quickly. Our team can focus on patient care instead of tech problems."
|
"Bay Area IT transformed our IT infrastructure completely. Their proactive approach means we rarely have downtime, and when issues do arise, they're resolved quickly. Our team can focus on patient care instead of tech problems."
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 relative z-10">
|
<div className="flex items-center gap-4 relative z-10">
|
||||||
|
|||||||
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: bay-area-affiliates:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:80"
|
||||||
38
fix-encoding.mjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
|
const content = readFileSync('src/data/seoData.ts', 'utf8');
|
||||||
|
|
||||||
|
// These are the mojibake sequences (UTF-8 bytes misread as Latin-1)
|
||||||
|
// and their correct Unicode replacements
|
||||||
|
const replacements = [
|
||||||
|
['â€"', '—'], // em dash
|
||||||
|
['â€"', '–'], // en dash (different byte sequence)
|
||||||
|
['’', '\u2019'], // right single quote / apostrophe
|
||||||
|
['‘', '\u2018'], // left single quote
|
||||||
|
['“', '\u201c'], // left double quote
|
||||||
|
['â€', '\u201d'], // right double quote
|
||||||
|
['…', '…'], // ellipsis
|
||||||
|
['•', '•'], // bullet
|
||||||
|
];
|
||||||
|
|
||||||
|
let fixed = content;
|
||||||
|
for (const [bad, good] of replacements) {
|
||||||
|
const count = fixed.split(bad).length - 1;
|
||||||
|
if (count > 0) console.log(`Replacing ${count}x: "${bad}" → "${good}"`);
|
||||||
|
fixed = fixed.split(bad).join(good);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any remaining â sequences
|
||||||
|
const remaining = fixed.match(/â[^\w\s'"]{1,3}/g);
|
||||||
|
if (remaining) {
|
||||||
|
console.log('Remaining unhandled sequences:', [...new Set(remaining)]);
|
||||||
|
} else {
|
||||||
|
console.log('No remaining mojibake found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content !== fixed) {
|
||||||
|
writeFileSync('src/data/seoData.ts', fixed, 'utf8');
|
||||||
|
console.log('File saved successfully.');
|
||||||
|
} else {
|
||||||
|
console.log('No changes needed.');
|
||||||
|
}
|
||||||
78
index.html
@@ -3,87 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Bay Area Affiliates, Inc. - Modern IT Solutions</title>
|
<title>Bay Area IT - Managed IT Services in Corpus Christi</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
<!-- Tailwind CSS -->
|
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
darkMode: "class",
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: "#FFFFFF",
|
|
||||||
"background-light": "#F3F4F6",
|
|
||||||
"background-dark": "#0a0a0a",
|
|
||||||
"surface-dark": "#121212",
|
|
||||||
"text-muted": "#888888",
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
display: ['"Space Grotesk"', 'sans-serif'],
|
|
||||||
sans: ['"Inter"', 'sans-serif'],
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
DEFAULT: "4px",
|
|
||||||
'lg': "8px",
|
|
||||||
'xl': "12px",
|
|
||||||
},
|
|
||||||
backgroundImage: {
|
|
||||||
'grain': "url('data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.05%22/%3E%3C/svg%3E')",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
height: auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background-color: #0a0a0a;
|
|
||||||
color: white;
|
|
||||||
overflow-x: hidden;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.lenis.lenis-smooth {
|
|
||||||
scroll-behavior: auto !important;
|
|
||||||
}
|
|
||||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
.lenis.lenis-stopped {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.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">
|
<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>
|
||||||
<script type="module" src="/index.tsx"></script>
|
<script type="module" src="/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
25
index.tsx
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import './src/index.css';
|
||||||
const rootElement = document.getElementById('root');
|
|
||||||
if (!rootElement) {
|
const rootElement = document.getElementById('root');
|
||||||
throw new Error("Could not find root element to mount to");
|
if (!rootElement) {
|
||||||
}
|
throw new Error("Could not find root element to mount to");
|
||||||
|
}
|
||||||
const root = ReactDOM.createRoot(rootElement);
|
|
||||||
root.render(
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
<React.StrictMode>
|
root.render(
|
||||||
<App />
|
<React.StrictMode>
|
||||||
</React.StrictMode>
|
<App />
|
||||||
);
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "Bay Area Affiliates",
|
"name": "Bay Area Affiliates",
|
||||||
"description": "Modern IT Solutions corporate website featuring smooth GSAP scrolling, Framer Motion animations, and a premium dark-mode aesthetic.",
|
"description": "Modern IT Solutions corporate website featuring smooth GSAP scrolling, Framer Motion animations, and a premium dark-mode aesthetic.",
|
||||||
"requestFramePermissions": []
|
"requestFramePermissions": []
|
||||||
}
|
}
|
||||||
6458
package-lock.json
generated
52
package.json
@@ -1,27 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "bay-area-affiliates",
|
"name": "bay-area-affiliates",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "tsx scripts/generate-sitemap.ts && tsx scripts/generate-robots.ts && vite build && node scripts/prune-dist-assets.mjs && tsx scripts/prerender-routes.ts",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate:seo": "npx tsx scripts/generate-sitemap.ts && npx tsx scripts/generate-robots.ts"
|
"generate:seo": "tsx scripts/generate-sitemap.ts && tsx scripts/generate-robots.ts",
|
||||||
|
"optimize:images": "node scripts/optimize-images.mjs",
|
||||||
|
"prerender:routes": "tsx scripts/prerender-routes.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@studio-freight/lenis": "^1.0.42",
|
"@studio-freight/lenis": "^1.0.42",
|
||||||
"framer-motion": "^12.26.2",
|
"framer-motion": "^12.26.2",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-router-dom": "^7.12.0"
|
"react-router-dom": "^7.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"tsx": "^4.21.0",
|
"@types/node": "^22.14.0",
|
||||||
"typescript": "~5.8.2",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"vite": "^6.2.0"
|
"autoprefixer": "^10.4.27",
|
||||||
}
|
"postcss": "^8.5.8",
|
||||||
}
|
"sharp": "^0.34.5",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/assets/services/business-it.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/assets/services/desktop-hardware.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/assets/services/help-desk.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
public/assets/services/managed-it.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/assets/services/nas-storage.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/assets/services/network-infrastructure.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/assets/services/printer-scanner.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/assets/services/vpn-setup.webp
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
public/images/blog/aransas-pass-it.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/images/blog/business-email-comparison-new.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
public/images/blog/business-email-comparison.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/images/blog/business-email-comparison.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/images/blog/common-it-problems.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
public/images/blog/corpus-christi-it.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
public/images/blog/it-service-comparison.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/images/blog/it-support-cost.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/images/blog/it-support-small-business.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/images/blog/kingsville-tx-it.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/images/blog/outsourced-it-support.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/images/blog/portland-tx-it.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/images/blog/rockport-tx-it.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
41
public/logo.svg
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
viewBox="0 0 10.583333 10.583333"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g2"
|
||||||
|
transform="translate(-59.849599,-62.962344)">
|
||||||
|
<g
|
||||||
|
id="g3"
|
||||||
|
transform="matrix(2.4622373,0,0,2.4622373,-103.30064,-98.368198)">
|
||||||
|
<path
|
||||||
|
id="rect4"
|
||||||
|
style="opacity:0.92;fill:#000080;fill-opacity:0;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
|
||||||
|
d="m 69.557843,68.36669 v -1.921262 h -1.960031 v 1.445588 c 0,0.26351 0.212068,0.475674 0.475578,0.475674 z" />
|
||||||
|
<path
|
||||||
|
id="path1"
|
||||||
|
style="opacity:0.92;fill:#000080;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
|
||||||
|
d="m 68.07339,65.851028 c -0.26351,0 -0.475578,0.212067 -0.475578,0.475579 v 0.118821 h 1.960031 v 1.921262 h 0.10113 c 0.26351,0 0.475578,-0.212164 0.475578,-0.475674 v -1.564409 c 0,-0.263512 -0.212068,-0.475579 -0.475578,-0.475579 z" />
|
||||||
|
<path
|
||||||
|
id="rect14"
|
||||||
|
style="opacity:0.92;fill:#0000ff;fill-opacity:0;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
|
||||||
|
d="m 68.587351,69.424584 v -1.960128 h -1.99822 v 1.484453 c 0,0.26351 0.212067,0.475675 0.475577,0.475675 z" />
|
||||||
|
<path
|
||||||
|
id="path2"
|
||||||
|
style="opacity:0.92;fill:#0000ff;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
|
||||||
|
d="m 67.064708,66.908921 c -0.26351,0 -0.475577,0.212068 -0.475577,0.475578 v 0.07996 h 1.99822 v 1.960128 h 0.06294 c 0.263512,0 0.475579,-0.212165 0.475579,-0.475675 v -1.56441 c 0,-0.26351 -0.212067,-0.475578 -0.475579,-0.475578 z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -2,145 +2,133 @@
|
|||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services</loc>
|
<loc>https://bayareait.services</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/services</loc>
|
<loc>https://bayareait.services/services</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog</loc>
|
<loc>https://bayareait.services/blog</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/contact</loc>
|
<loc>https://bayareait.services/contact</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/about</loc>
|
<loc>https://bayareait.services/about</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/it-support-corpus-christi</loc>
|
<loc>https://bayareait.services/locations/it-support-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/it-support-portland-tx</loc>
|
<loc>https://bayareait.services/locations/it-support-portland-tx</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/it-support-rockport-tx</loc>
|
<loc>https://bayareait.services/locations/it-support-rockport-tx</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/it-support-aransas-pass-tx</loc>
|
<loc>https://bayareait.services/locations/it-support-aransas-pass-tx</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/it-support-kingsville-tx</loc>
|
<loc>https://bayareait.services/locations/it-support-kingsville-tx</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/business-it-support</loc>
|
<loc>https://bayareait.services/services/web-design-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/it-help-desk</loc>
|
<loc>https://bayareait.services/services/business-email-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/computer-support</loc>
|
<loc>https://bayareait.services/services/it-help-desk</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/managed-it-services-corpus-christi</loc>
|
<loc>https://bayareait.services/services/computer-support</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://bayareait.services/services/business-email-services</loc>
|
||||||
|
<lastmod>2026-03-25</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://bayareait.services/services/domain-registration-dns-support</loc>
|
||||||
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog/it-support-small-business-corpus-christi</loc>
|
<loc>https://bayareait.services/blog/it-support-small-business-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog/outsourced-it-support-corpus-christi</loc>
|
<loc>https://bayareait.services/blog/outsourced-it-support-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog/it-service-vs-inhouse-it</loc>
|
<loc>https://bayareait.services/blog/it-service-vs-inhouse-it</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog/common-it-problems-businesses-corpus-christi</loc>
|
<loc>https://bayareait.services/blog/common-it-problems-businesses-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog/it-support-cost-corpus-christi</loc>
|
<loc>https://bayareait.services/blog/it-support-cost-corpus-christi</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://bayareait.services/blog/it-support-corpus-christi-blog</loc>
|
<loc>https://bayareait.services/blog/business-email-vs-google-workspace-vs-microsoft-365</loc>
|
||||||
<lastmod>2026-01-22</lastmod>
|
<lastmod>2026-03-25</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>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import fs from 'fs';
|
import path from 'path';
|
||||||
import path from 'path';
|
|
||||||
|
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
||||||
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
|
||||||
|
const generateRobots = () => {
|
||||||
const generateRobots = () => {
|
const content = `User-agent: *
|
||||||
const content = `User-agent: *
|
Allow: /
|
||||||
Allow: /
|
Disallow: /admin
|
||||||
Disallow: /admin
|
Disallow: /api
|
||||||
Disallow: /api
|
|
||||||
|
Sitemap: ${BASE_URL}/sitemap.xml
|
||||||
Sitemap: ${BASE_URL}/sitemap.xml
|
`;
|
||||||
`;
|
return content;
|
||||||
return content;
|
};
|
||||||
};
|
|
||||||
|
const robots = generateRobots();
|
||||||
const robots = generateRobots();
|
const outputPath = path.resolve(process.cwd(), 'public/robots.txt');
|
||||||
const outputPath = path.resolve(process.cwd(), 'public/robots.txt');
|
const publicDir = path.dirname(outputPath);
|
||||||
|
|
||||||
// Ensure public directory exists
|
if (!fs.existsSync(publicDir)) {
|
||||||
const publicDir = path.dirname(outputPath);
|
fs.mkdirSync(publicDir, { recursive: true });
|
||||||
if (!fs.existsSync(publicDir)) {
|
}
|
||||||
fs.mkdirSync(publicDir, { recursive: true });
|
|
||||||
}
|
fs.writeFileSync(outputPath, robots);
|
||||||
|
console.log(`Robots.txt generated at ${outputPath}`);
|
||||||
fs.writeFileSync(outputPath, robots);
|
|
||||||
console.log(`✅ Robots.txt generated at ${outputPath}`);
|
|
||||||
|
|||||||
@@ -1,85 +1,78 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import fs from 'fs';
|
import path from 'path';
|
||||||
import path from 'path';
|
import { locationData, serviceData, blogPostData } from '../src/data/seoData';
|
||||||
import { locationData, serviceData, blogPostData } from '../src/data/seoData';
|
|
||||||
|
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
||||||
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
|
||||||
|
const generateSitemap = () => {
|
||||||
/**
|
const currentDate = new Date().toISOString().split('T')[0];
|
||||||
* Generates the sitemap.xml content
|
|
||||||
*/
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
const generateSitemap = () => {
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
const currentDate = new Date().toISOString().split('T')[0];
|
`;
|
||||||
|
|
||||||
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
const staticPages = [
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
'',
|
||||||
`;
|
'/locations',
|
||||||
|
'/services',
|
||||||
// Static Pages
|
'/blog',
|
||||||
const staticPages = [
|
'/contact',
|
||||||
'',
|
'/about',
|
||||||
'/services',
|
'/privacy-policy',
|
||||||
'/blog',
|
'/terms-of-service'
|
||||||
'/contact',
|
];
|
||||||
'/about'
|
|
||||||
];
|
staticPages.forEach(page => {
|
||||||
|
xml += ` <url>
|
||||||
staticPages.forEach(page => {
|
<loc>${BASE_URL}${page}</loc>
|
||||||
xml += ` <url>
|
<lastmod>${currentDate}</lastmod>
|
||||||
<loc>${BASE_URL}${page}</loc>
|
<changefreq>monthly</changefreq>
|
||||||
<lastmod>${currentDate}</lastmod>
|
<priority>${page === '' ? '1.0' : '0.8'}</priority>
|
||||||
<changefreq>monthly</changefreq>
|
</url>
|
||||||
<priority>${page === '' ? '1.0' : '0.8'}</priority>
|
`;
|
||||||
</url>
|
});
|
||||||
`;
|
|
||||||
});
|
locationData.forEach(page => {
|
||||||
|
xml += ` <url>
|
||||||
// Location Pages
|
<loc>${BASE_URL}/${page.slug}</loc>
|
||||||
locationData.forEach(page => {
|
<lastmod>${currentDate}</lastmod>
|
||||||
xml += ` <url>
|
<changefreq>weekly</changefreq>
|
||||||
<loc>${BASE_URL}/${page.slug}</loc>
|
<priority>0.9</priority>
|
||||||
<lastmod>${currentDate}</lastmod>
|
</url>
|
||||||
<changefreq>weekly</changefreq>
|
`;
|
||||||
<priority>0.9</priority>
|
});
|
||||||
</url>
|
|
||||||
`;
|
serviceData.forEach(page => {
|
||||||
});
|
xml += ` <url>
|
||||||
|
<loc>${BASE_URL}/${page.slug}</loc>
|
||||||
// Service Pages
|
<lastmod>${currentDate}</lastmod>
|
||||||
serviceData.forEach(page => {
|
<changefreq>weekly</changefreq>
|
||||||
xml += ` <url>
|
<priority>0.9</priority>
|
||||||
<loc>${BASE_URL}/${page.slug}</loc>
|
</url>
|
||||||
<lastmod>${currentDate}</lastmod>
|
`;
|
||||||
<changefreq>weekly</changefreq>
|
});
|
||||||
<priority>0.9</priority>
|
|
||||||
</url>
|
blogPostData.filter(post => !('redirect' in post) || !post.redirect).forEach(post => {
|
||||||
`;
|
xml += ` <url>
|
||||||
});
|
<loc>${BASE_URL}/${post.slug}</loc>
|
||||||
|
<lastmod>${currentDate}</lastmod>
|
||||||
// Blog Posts
|
<changefreq>monthly</changefreq>
|
||||||
blogPostData.forEach(post => {
|
<priority>0.7</priority>
|
||||||
xml += ` <url>
|
</url>
|
||||||
<loc>${BASE_URL}/blog/${post.slug}</loc>
|
`;
|
||||||
<lastmod>${currentDate}</lastmod>
|
});
|
||||||
<changefreq>monthly</changefreq>
|
|
||||||
<priority>0.7</priority>
|
xml += `</urlset>`;
|
||||||
</url>
|
return xml;
|
||||||
`;
|
};
|
||||||
});
|
|
||||||
|
const sitemap = generateSitemap();
|
||||||
xml += `</urlset>`;
|
const outputPath = path.resolve(process.cwd(), 'public/sitemap.xml');
|
||||||
return xml;
|
const publicDir = path.dirname(outputPath);
|
||||||
};
|
|
||||||
|
if (!fs.existsSync(publicDir)) {
|
||||||
// Write to public/sitemap.xml
|
fs.mkdirSync(publicDir, { recursive: true });
|
||||||
const sitemap = generateSitemap();
|
}
|
||||||
const outputPath = path.resolve(process.cwd(), 'public/sitemap.xml');
|
|
||||||
|
fs.writeFileSync(outputPath, sitemap);
|
||||||
// Ensure public directory exists
|
console.log(`Sitemap generated at ${outputPath}`);
|
||||||
const publicDir = path.dirname(outputPath);
|
|
||||||
if (!fs.existsSync(publicDir)) {
|
|
||||||
fs.mkdirSync(publicDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(outputPath, sitemap);
|
|
||||||
console.log(`✅ Sitemap generated at ${outputPath}`);
|
|
||||||
|
|||||||
61
scripts/optimize-images.mjs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const roots = ['public', path.join('src', 'assets')];
|
||||||
|
|
||||||
|
async function walk(dir) {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
const files = await Promise.all(
|
||||||
|
entries.map(async (entry) => {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return walk(fullPath);
|
||||||
|
}
|
||||||
|
return fullPath;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return files.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickWidth(filePath) {
|
||||||
|
const normalized = filePath.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
if (normalized.includes('/images/blog/')) return 1280;
|
||||||
|
if (normalized.endsWith('/hero-bg.png')) return 1600;
|
||||||
|
if (normalized.endsWith('/process-illustration.png')) return 1600;
|
||||||
|
if (normalized.includes('/assets/services/')) return 1200;
|
||||||
|
|
||||||
|
return 1280;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optimize(filePath) {
|
||||||
|
const targetPath = filePath.replace(/\.png$/i, '.webp');
|
||||||
|
const width = pickWidth(filePath);
|
||||||
|
const image = sharp(filePath, { animated: false }).rotate();
|
||||||
|
const metadata = await image.metadata();
|
||||||
|
|
||||||
|
const pipeline =
|
||||||
|
metadata.width && metadata.width > width ? image.resize({ width, withoutEnlargement: true }) : image;
|
||||||
|
|
||||||
|
await pipeline.webp({ quality: 76, effort: 6 }).toFile(targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pngFiles = (
|
||||||
|
await Promise.all(
|
||||||
|
roots.map(async (root) => {
|
||||||
|
try {
|
||||||
|
return await walk(root);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.filter((filePath) => filePath.toLowerCase().endsWith('.png'));
|
||||||
|
|
||||||
|
await Promise.all(pngFiles.map((filePath) => optimize(filePath)));
|
||||||
|
|
||||||
|
console.log(`Optimized ${pngFiles.length} PNG files to WebP.`);
|
||||||
206
scripts/prerender-routes.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { blogPostData, locationData, serviceData } from '../src/data/seoData';
|
||||||
|
|
||||||
|
type RouteMeta = {
|
||||||
|
route: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
canonicalUrl: string;
|
||||||
|
keywords?: string[];
|
||||||
|
schema?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIST_DIR = path.resolve(process.cwd(), 'dist');
|
||||||
|
const BASE_URL = 'https://bayareait.services';
|
||||||
|
const DEFAULT_OG_IMAGE = `${BASE_URL}/logo.svg`;
|
||||||
|
|
||||||
|
const staticRoutes: RouteMeta[] = [
|
||||||
|
{
|
||||||
|
route: '/',
|
||||||
|
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 and help desk solutions.',
|
||||||
|
canonicalUrl: `${BASE_URL}/`,
|
||||||
|
keywords: ['IT Service', 'IT Support', 'Corpus Christi', 'IT Help Desk'],
|
||||||
|
schema: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'ITService',
|
||||||
|
name: 'Bay Area IT',
|
||||||
|
url: BASE_URL,
|
||||||
|
telephone: '+1-361-765-8400',
|
||||||
|
areaServed: ['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/about',
|
||||||
|
title: 'About Bay Area IT | Local IT Support in Corpus Christi',
|
||||||
|
description:
|
||||||
|
'Learn about Bay Area IT, a local IT partner serving Corpus Christi and the Coastal Bend with practical support, reliable service, and over 25 years of experience.',
|
||||||
|
canonicalUrl: `${BASE_URL}/about`,
|
||||||
|
schema: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Bay Area IT',
|
||||||
|
url: BASE_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/services',
|
||||||
|
title: 'IT Services | Bay Area IT Support, Email, Networking and Web',
|
||||||
|
description:
|
||||||
|
'Explore Bay Area IT services for Corpus Christi businesses, including help desk support, business email, networking, hardware, web design, and day-to-day IT support.',
|
||||||
|
canonicalUrl: `${BASE_URL}/services`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/blog',
|
||||||
|
title: 'Blog | Bay Area IT Insights for Corpus Christi Businesses',
|
||||||
|
description:
|
||||||
|
'Read practical IT guidance for Corpus Christi and Coastal Bend businesses, from managed IT support and costs to business email and local service coverage.',
|
||||||
|
canonicalUrl: `${BASE_URL}/blog`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/contact',
|
||||||
|
title: 'Contact Bay Area IT | Free IT Assessment in Corpus Christi',
|
||||||
|
description:
|
||||||
|
'Talk to Bay Area IT about managed IT support, help desk coverage, business email, networking, and technology support across Corpus Christi and the Coastal Bend.',
|
||||||
|
canonicalUrl: `${BASE_URL}/contact`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/locations',
|
||||||
|
title: 'IT Support Service Areas - Corpus Christi & Coastal Bend, TX',
|
||||||
|
description:
|
||||||
|
'Bay Area IT provides IT support and IT services throughout the Coastal Bend. View all cities we serve in the Corpus Christi area.',
|
||||||
|
canonicalUrl: `${BASE_URL}/locations`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/privacy-policy',
|
||||||
|
title: 'Privacy Policy | Bay Area IT',
|
||||||
|
description:
|
||||||
|
'Read how Bay Area IT collects, uses, and protects information submitted through this website.',
|
||||||
|
canonicalUrl: `${BASE_URL}/privacy-policy`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: '/terms-of-service',
|
||||||
|
title: 'Terms of Service | Bay Area IT',
|
||||||
|
description:
|
||||||
|
'Review the Bay Area IT terms covering use of this website and our IT support services.',
|
||||||
|
canonicalUrl: `${BASE_URL}/terms-of-service`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dynamicRoutes: RouteMeta[] = [
|
||||||
|
...locationData.map((item) => ({
|
||||||
|
route: `/${item.slug}`,
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
canonicalUrl: `${BASE_URL}/${item.slug}`,
|
||||||
|
keywords: item.keywords,
|
||||||
|
schema: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'LocalBusiness',
|
||||||
|
name: 'Bay Area IT',
|
||||||
|
url: `${BASE_URL}/${item.slug}`,
|
||||||
|
areaServed: item.city,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...serviceData.map((item) => ({
|
||||||
|
route: `/${item.slug}`,
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
canonicalUrl: `${BASE_URL}/${item.slug}`,
|
||||||
|
keywords: item.keywords,
|
||||||
|
schema: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Service',
|
||||||
|
name: item.h1,
|
||||||
|
provider: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Bay Area IT',
|
||||||
|
},
|
||||||
|
url: `${BASE_URL}/${item.slug}`,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...blogPostData
|
||||||
|
.filter((item) => !item.redirect)
|
||||||
|
.map((item) => ({
|
||||||
|
route: `/${item.slug}`,
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
canonicalUrl: `${BASE_URL}/${item.slug}`,
|
||||||
|
keywords: item.keywords,
|
||||||
|
schema: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BlogPosting',
|
||||||
|
headline: item.h1,
|
||||||
|
description: item.description,
|
||||||
|
url: `${BASE_URL}/${item.slug}`,
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Bay Area IT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
function escapeHtml(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHead(meta: RouteMeta) {
|
||||||
|
const keywords = meta.keywords?.length
|
||||||
|
? `\n <meta name="keywords" content="${escapeHtml(meta.keywords.join(', '))}" />`
|
||||||
|
: '';
|
||||||
|
const schema = meta.schema
|
||||||
|
? `\n <script type="application/ld+json">${JSON.stringify(meta.schema)}</script>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return ` <title>${escapeHtml(meta.title)}</title>
|
||||||
|
<meta name="description" content="${escapeHtml(meta.description)}" />${keywords}
|
||||||
|
<link rel="canonical" href="${meta.canonicalUrl}" />
|
||||||
|
<meta property="og:title" content="${escapeHtml(meta.title)}" />
|
||||||
|
<meta property="og:description" content="${escapeHtml(meta.description)}" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="Bay Area IT" />
|
||||||
|
<meta property="og:url" content="${meta.canonicalUrl}" />
|
||||||
|
<meta property="og:image" content="${DEFAULT_OG_IMAGE}" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:title" content="${escapeHtml(meta.title)}" />
|
||||||
|
<meta name="twitter:description" content="${escapeHtml(meta.description)}" />${schema}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectHead(template: string, meta: RouteMeta) {
|
||||||
|
const withoutTitle = template.replace(/<title>[\s\S]*?<\/title>/i, '');
|
||||||
|
return withoutTitle.replace('</head>', `${buildHead(meta)}\n </head>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRouteHtml(template: string, meta: RouteMeta) {
|
||||||
|
const html = injectHead(template, meta);
|
||||||
|
const cleanRoute = meta.route === '/' ? '' : meta.route.replace(/^\/+/, '');
|
||||||
|
const filePath =
|
||||||
|
meta.route === '/'
|
||||||
|
? path.join(DIST_DIR, 'index.html')
|
||||||
|
: path.join(DIST_DIR, cleanRoute, 'index.html');
|
||||||
|
|
||||||
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
await fs.writeFile(filePath, html, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const templatePath = path.join(DIST_DIR, 'index.html');
|
||||||
|
const template = await fs.readFile(templatePath, 'utf8');
|
||||||
|
const allRoutes = [...staticRoutes, ...dynamicRoutes];
|
||||||
|
|
||||||
|
await Promise.all(allRoutes.map((meta) => writeRouteHtml(template, meta)));
|
||||||
|
console.log(`Prerendered ${allRoutes.length} route HTML files.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Failed to prerender route HTML files.');
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
42
scripts/prune-dist-assets.mjs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const distDir = 'dist';
|
||||||
|
|
||||||
|
async function walk(dir) {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
const files = await Promise.all(
|
||||||
|
entries.map(async (entry) => {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return walk(fullPath);
|
||||||
|
}
|
||||||
|
return fullPath;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return files.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await walk(distDir);
|
||||||
|
const pngFiles = files.filter((filePath) => filePath.toLowerCase().endsWith('.png'));
|
||||||
|
|
||||||
|
let removed = 0;
|
||||||
|
|
||||||
|
for (const pngFile of pngFiles) {
|
||||||
|
const webpFile = pngFile.replace(/\.png$/i, '.webp');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(webpFile);
|
||||||
|
await fs.unlink(pngFile);
|
||||||
|
removed += 1;
|
||||||
|
} catch {
|
||||||
|
// Keep PNGs that have no optimized WebP counterpart.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Pruned ${removed} PNG files from dist.`);
|
||||||
|
} catch {
|
||||||
|
console.log('No dist directory to prune.');
|
||||||
|
}
|
||||||
BIN
src/assets/hero-bg.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/assets/process-illustration.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
63
src/index.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
height: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-smooth {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-smooth [data-lenis-prevent] {
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-stopped {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-scrolling iframe {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { motion, useInView, useSpring, useTransform, useScroll, useMotionValueEvent } from 'framer-motion';
|
import { motion, useInView, useSpring, useTransform, useScroll, useMotionValueEvent } from 'framer-motion';
|
||||||
import Contact from '../../components/Contact';
|
import Contact from '../../components/Contact';
|
||||||
|
import SEO from '../../components/SEO';
|
||||||
|
|
||||||
const Counter = ({ value }: { value: number }) => {
|
const Counter = ({ value }: { value: number }) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@@ -40,13 +41,6 @@ const AboutPage: React.FC = () => {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{ label: 'Businesses served', value: '150+' },
|
|
||||||
{ label: 'Uptime achieved', value: '99.9%' },
|
|
||||||
{ label: 'Years of service', value: '15+' },
|
|
||||||
{ label: 'Response time', value: '<2min' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
{
|
{
|
||||||
title: 'Security-First',
|
title: 'Security-First',
|
||||||
@@ -66,14 +60,21 @@ const AboutPage: React.FC = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const timeline = [
|
const timeline = [
|
||||||
{ year: '2010', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
|
{ year: '2000', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
|
||||||
{ year: '2015', title: 'Expanded Service Portfolio', desc: 'Added cloud services and advanced networking to serve growing businesses.' },
|
{ year: '2015', title: 'Expanded Service Portfolio', desc: 'Added cloud services and advanced networking to serve growing businesses.' },
|
||||||
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped 100+ businesses transition to secure remote work during the pandemic.' },
|
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped local businesses strengthen remote access, security, and day-to-day support during a disruptive period.' },
|
||||||
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now serving 150+ businesses with modern, reliable IT infrastructure.' },
|
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now supporting 30+ local businesses with practical, reliable IT infrastructure.' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
<>
|
||||||
|
<SEO
|
||||||
|
title="About Bay Area IT | Local IT Support in Corpus Christi"
|
||||||
|
description="Learn about Bay Area IT, a local IT partner serving Corpus Christi and the Coastal Bend with practical support, reliable service, and over 25 years of experience."
|
||||||
|
keywords={['about Bay Area IT', 'Corpus Christi IT company', 'local IT support Coastal Bend']}
|
||||||
|
canonicalUrl="https://bayareait.services/about"
|
||||||
|
/>
|
||||||
|
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
||||||
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
@@ -92,7 +93,7 @@ const AboutPage: React.FC = () => {
|
|||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"
|
className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"
|
||||||
>
|
>
|
||||||
Since 2010, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that drive growth.
|
Since 2000, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that keep work moving.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -104,13 +105,13 @@ const AboutPage: React.FC = () => {
|
|||||||
<h2 className="font-display text-3xl font-bold mb-8 text-gray-900 dark:text-white">Our Story</h2>
|
<h2 className="font-display text-3xl font-bold mb-8 text-gray-900 dark:text-white">Our Story</h2>
|
||||||
<div className="prose dark:prose-invert max-w-none text-lg text-gray-600 dark:text-gray-300 space-y-6">
|
<div className="prose dark:prose-invert max-w-none text-lg text-gray-600 dark:text-gray-300 space-y-6">
|
||||||
<p>
|
<p>
|
||||||
Bay Area Affiliates was founded with a simple belief: local businesses deserve the same level of IT expertise and reliability as large corporations, but with the personal touch that only comes from working with your neighbors.
|
Bay Area IT was founded with a simple belief: local businesses deserve dependable technology support without enterprise complexity, vague communication, or reactive chaos.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Over the years, we've watched the Coastal Bend grow and change. We've helped businesses navigate technology challenges, from the transition to cloud computing to the rapid shift to remote work. Through it all, we've maintained our commitment to clear communication, reliable solutions, and exceptional service.
|
Over the years, we've watched the Coastal Bend grow and change. We've helped businesses navigate technology challenges, from the transition to cloud computing to the rapid shift to remote work. Through it all, we've maintained our commitment to clear communication, reliable solutions, and exceptional service.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Today, we're proud to serve over 150 businesses across the region, from Corpus Christi to the smallest coastal communities. Our team combines deep technical expertise with real-world business understanding to deliver IT solutions that actually work for our clients.
|
Today, we're proud to support 30+ local businesses across the region. Our team combines deep technical experience with real-world business judgment to deliver IT support that is clear, practical, and reliable.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,10 +122,10 @@ const AboutPage: React.FC = () => {
|
|||||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))] pointer-events-none"></div>
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))] pointer-events-none"></div>
|
||||||
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-center relative z-10">
|
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-center relative z-10">
|
||||||
{[
|
{[
|
||||||
{ label: 'Businesses served', value: 150, suffix: '+' },
|
{ label: 'Businesses served', value: 30, suffix: '+' },
|
||||||
{ label: 'Uptime achieved', value: 99.9, suffix: '%' },
|
{ label: 'Uptime achieved', value: 99.9, suffix: '%' },
|
||||||
{ label: 'Years of service', value: 15, suffix: '+' },
|
{ label: 'Years of service', value: 25, suffix: '+' },
|
||||||
{ label: 'Response time', value: 2, prefix: '<', suffix: 'min' },
|
{ label: 'Response time', value: 2, prefix: '<', suffix: 'min' },
|
||||||
].map((stat, index) => (
|
].map((stat, index) => (
|
||||||
<div key={index} className="p-4">
|
<div key={index} className="p-4">
|
||||||
<div className="text-4xl md:text-5xl font-bold mb-2 flex justify-center items-center gap-1">
|
<div className="text-4xl md:text-5xl font-bold mb-2 flex justify-center items-center gap-1">
|
||||||
@@ -238,9 +239,10 @@ const AboutPage: React.FC = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Contact />
|
<Contact />
|
||||||
</div>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export default AboutPage;
|
export default AboutPage;
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
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 { Link } from 'react-router-dom';
|
||||||
import Contact from '../../components/Contact';
|
import Contact from '../../components/Contact';
|
||||||
import { blogPostData } from '../data/seoData';
|
import SEO from '../../components/SEO';
|
||||||
|
import { blogPostData } from '../data/seoData';
|
||||||
const BlogPage: React.FC = () => {
|
|
||||||
useEffect(() => {
|
const cardVariants = {
|
||||||
window.scrollTo(0, 0);
|
hidden: { opacity: 0, y: 20 },
|
||||||
}, []);
|
visible: (index: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.45,
|
||||||
|
delay: Math.min(index * 0.06, 0.3),
|
||||||
|
ease: 'easeOut',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlogPage: React.FC = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
<>
|
||||||
|
<SEO
|
||||||
|
title="Blog | Bay Area IT Insights for Corpus Christi Businesses"
|
||||||
|
description="Read practical IT guidance for Corpus Christi and Coastal Bend businesses, from managed IT support and costs to business email and local service coverage."
|
||||||
|
keywords={['Corpus Christi IT blog', 'managed IT insights', 'business IT support articles']}
|
||||||
|
canonicalUrl="https://bayareait.services/blog"
|
||||||
|
/>
|
||||||
|
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
||||||
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
||||||
<div className="absolute top-[300px] left-1/2 -translate-x-1/2 w-[800px] h-[800px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
|
<div className="absolute top-[300px] left-1/2 -translate-x-1/2 w-[800px] h-[800px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
|
||||||
<section className="py-20 px-6 bg-white dark:bg-[#0f0f0f] border-b border-gray-100 dark:border-white/5 relative bg-transparent">
|
<section className="py-20 px-6 bg-white dark:bg-[#0f0f0f] border-b border-gray-100 dark:border-white/5 relative bg-transparent">
|
||||||
@@ -24,23 +45,27 @@ 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">
|
||||||
{blogPostData.map((post) => (
|
{blogPostData.filter(post => !post.redirect).map((post, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={post.id}
|
key={post.id}
|
||||||
to={`/${post.slug}`}
|
to={`/${post.slug}`}
|
||||||
className="block"
|
className="block"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial="hidden"
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
animate="visible"
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
custom={index}
|
||||||
whileHover={{ y: -5 }}
|
variants={cardVariants}
|
||||||
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"
|
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">
|
<div className="h-64 md:h-auto overflow-hidden relative">
|
||||||
<img
|
<img
|
||||||
src={post.image || '/images/blog/default.png'}
|
src={post.image || '/images/blog/default.png'}
|
||||||
alt={post.h1}
|
alt={post.h1}
|
||||||
|
loading={index === 0 ? 'eager' : 'lazy'}
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority={index === 0 ? 'high' : 'low'}
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-4 left-4">
|
<div className="absolute top-4 left-4">
|
||||||
@@ -73,9 +98,10 @@ const BlogPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Contact />
|
<Contact />
|
||||||
</div>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default BlogPage;
|
export default BlogPage;
|
||||||
|
|||||||
@@ -1,322 +1,236 @@
|
|||||||
import React, { useEffect, useRef, useLayoutEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import gsap from 'gsap';
|
import SEO from '../../components/SEO';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import Services from '../../components/Services';
|
||||||
import SEO from '../../components/SEO';
|
import CTA from '../../components/CTA';
|
||||||
import Services from '../../components/Services';
|
import AreasWeServe from '../../components/AreasWeServe';
|
||||||
import CTA from '../../components/CTA';
|
import { BlogPostData } from '../data/seoData';
|
||||||
import AreasWeServe from '../../components/AreasWeServe';
|
|
||||||
import { BlogPostData } from '../data/seoData';
|
interface BlogPostPageProps {
|
||||||
|
data: BlogPostData;
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
}
|
||||||
|
|
||||||
interface BlogPostPageProps {
|
const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
|
||||||
data: BlogPostData;
|
useEffect(() => {
|
||||||
}
|
window.scrollTo(0, 0);
|
||||||
|
}, []);
|
||||||
const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const category = data.slug.includes('corpus-christi-blog') ||
|
||||||
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
data.slug.includes('portland-tx') ||
|
||||||
const mouseX = useMotionValue(0);
|
data.slug.includes('rockport-tx') ||
|
||||||
const mouseY = useMotionValue(0);
|
data.slug.includes('aransas-pass') ||
|
||||||
|
data.slug.includes('kingsville-tx')
|
||||||
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
? 'Local Services'
|
||||||
const { left, top } = currentTarget.getBoundingClientRect();
|
: 'IT Insights';
|
||||||
mouseX.set(clientX - left);
|
|
||||||
mouseY.set(clientY - top + 75);
|
return (
|
||||||
};
|
<>
|
||||||
|
<SEO
|
||||||
useLayoutEffect(() => {
|
title={data.title}
|
||||||
const ctx = gsap.context(() => {
|
description={data.description}
|
||||||
// Parallax Background
|
keywords={data.keywords}
|
||||||
if (parallaxWrapperRef.current) {
|
canonicalUrl={`https://bayareait.services/${data.slug}`}
|
||||||
gsap.to(parallaxWrapperRef.current, {
|
/>
|
||||||
yPercent: 30,
|
|
||||||
ease: "none",
|
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden pt-32 pb-16">
|
||||||
scrollTrigger: {
|
{/* Clean Hero Section */}
|
||||||
trigger: containerRef.current,
|
<article className="max-w-4xl mx-auto px-6">
|
||||||
start: "top top",
|
<header className="mb-12 text-center">
|
||||||
end: "bottom top",
|
{/* Breadcrumbs */}
|
||||||
scrub: true
|
<nav className="mb-8 text-sm">
|
||||||
}
|
<ol className="flex items-center gap-2 text-gray-500 dark:text-gray-400 justify-center">
|
||||||
});
|
<li>
|
||||||
}
|
<Link to="/" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||||
|
Home
|
||||||
// Text Stagger Animation
|
</Link>
|
||||||
gsap.fromTo(".hero-stagger",
|
</li>
|
||||||
{ y: 50, opacity: 0 },
|
<span className="material-symbols-outlined text-xs">chevron_right</span>
|
||||||
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
|
<li>
|
||||||
);
|
<Link to="/blog" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||||
}, containerRef);
|
Blog
|
||||||
|
</Link>
|
||||||
return () => ctx.revert();
|
</li>
|
||||||
}, []);
|
<span className="material-symbols-outlined text-xs">chevron_right</span>
|
||||||
|
<li className="text-gray-900 dark:text-white font-medium">{category}</li>
|
||||||
useEffect(() => {
|
</ol>
|
||||||
window.scrollTo(0, 0);
|
</nav>
|
||||||
}, []);
|
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-6">
|
||||||
const category = data.slug.includes('corpus-christi-blog') ||
|
<span className="h-px w-8 bg-blue-600/30 dark:bg-blue-400/30"></span>
|
||||||
data.slug.includes('portland-tx') ||
|
<span className="text-xs uppercase tracking-[0.2em] text-blue-600 dark:text-blue-400 font-bold">
|
||||||
data.slug.includes('rockport-tx') ||
|
{category}
|
||||||
data.slug.includes('aransas-pass') ||
|
</span>
|
||||||
data.slug.includes('kingsville-tx')
|
<span className="h-px w-8 bg-blue-600/30 dark:bg-blue-400/30"></span>
|
||||||
? 'Local Services'
|
</div>
|
||||||
: 'IT Insights';
|
|
||||||
|
<h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight mb-6 text-gray-900 dark:text-white">
|
||||||
return (
|
{data.h1}
|
||||||
<>
|
</h1>
|
||||||
<SEO
|
|
||||||
title={data.title}
|
{/* Meta Info */}
|
||||||
description={data.description}
|
<div className="flex items-center gap-6 text-gray-500 dark:text-gray-400 mb-10 justify-center">
|
||||||
keywords={data.keywords}
|
<div className="flex items-center gap-2">
|
||||||
canonicalUrl={window.location.href}
|
<span className="material-symbols-outlined text-sm">schedule</span>
|
||||||
/>
|
<span>5 min read</span>
|
||||||
|
</div>
|
||||||
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
<div className="flex items-center gap-2">
|
||||||
{/* Hero Section */}
|
<span className="material-symbols-outlined text-sm">location_on</span>
|
||||||
<section
|
<span>Coastal Bend business guide</span>
|
||||||
ref={containerRef}
|
</div>
|
||||||
onMouseMove={handleMouseMove}
|
</div>
|
||||||
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
|
|
||||||
>
|
{/* Featured Image */}
|
||||||
{/* Parallax Background */}
|
{data.image && (
|
||||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
<motion.div
|
||||||
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
{/* Base Layer */}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<img
|
transition={{ duration: 0.6 }}
|
||||||
alt="Abstract dark technology background"
|
className="rounded-3xl overflow-hidden border border-gray-100 dark:border-white/5 shadow-xl md:mb-16 mb-8 max-w-4xl mx-auto"
|
||||||
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
>
|
||||||
src="/src/assets/hero-bg.png"
|
<img
|
||||||
/>
|
src={data.image}
|
||||||
|
alt={data.h1}
|
||||||
{/* Highlight Layer */}
|
loading="eager"
|
||||||
<motion.img
|
decoding="async"
|
||||||
style={{
|
fetchPriority="high"
|
||||||
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
|
className="w-full h-auto max-h-[500px] object-cover"
|
||||||
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
|
/>
|
||||||
}}
|
</motion.div>
|
||||||
alt=""
|
)}
|
||||||
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
</header>
|
||||||
src="/src/assets/hero-bg.png"
|
|
||||||
/>
|
{/* Main Content Section */}
|
||||||
</div>
|
<motion.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>
|
initial={{ opacity: 0 }}
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
|
animate={{ opacity: 1 }}
|
||||||
</div>
|
transition={{ delay: 0.2, duration: 0.6 }}
|
||||||
|
className="bg-white dark:bg-[#111] rounded-[2rem] p-8 md:p-12 lg:p-16 shadow-lg border border-gray-100 dark:border-white/5 mb-16"
|
||||||
{/* Hero Content */}
|
>
|
||||||
<div className="relative z-10 text-center max-w-4xl px-6">
|
<div className="prose prose-lg dark:prose-invert max-w-none prose-headings:font-display prose-headings:font-bold 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-img:rounded-xl prose-img:shadow-md">
|
||||||
{/* Breadcrumbs */}
|
<div dangerouslySetInnerHTML={{ __html: data.content }} />
|
||||||
<nav className="hero-stagger mb-8 text-sm">
|
</div>
|
||||||
<ol className="flex items-center gap-2 text-gray-600 dark:text-gray-400 justify-center">
|
</motion.div>
|
||||||
<li>
|
</article>
|
||||||
<Link to="/" className="hover:text-gray-900 dark:hover:text-white transition-colors">
|
|
||||||
Home
|
{/* CTA Section */}
|
||||||
</Link>
|
<section className="px-6 py-16">
|
||||||
</li>
|
<motion.div
|
||||||
<span className="material-symbols-outlined text-xs">chevron_right</span>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<li>
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
<Link to="/blog" className="hover:text-gray-900 dark:hover:text-white transition-colors">
|
viewport={{ once: true }}
|
||||||
Blog
|
className="max-w-4xl mx-auto"
|
||||||
</Link>
|
>
|
||||||
</li>
|
<div className="p-10 md:p-14 bg-gradient-to-br from-blue-50 to-gray-50 dark:from-blue-950/20 dark:to-gray-900/20 rounded-[2.5rem] border border-blue-100/50 dark:border-blue-900/30 shadow-xl text-center">
|
||||||
<span className="material-symbols-outlined text-xs">chevron_right</span>
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-6 block">
|
||||||
<li className="text-gray-900 dark:text-white font-medium">{category}</li>
|
{category === 'Local Services' ? 'location_on' : 'insights'}
|
||||||
</ol>
|
</span>
|
||||||
</nav>
|
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||||
|
{category === 'Local Services'
|
||||||
<div className="hero-stagger flex items-center justify-center gap-2 mb-6">
|
? 'Ready to Get IT Support in Your Area?'
|
||||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
: 'Need Expert IT Support for Your Business?'}
|
||||||
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">
|
</h2>
|
||||||
{category}
|
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||||
</span>
|
{category === 'Local Services'
|
||||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
? 'Contact us today to learn how we can help your business with reliable IT support and managed services.'
|
||||||
</div>
|
: 'Let us handle your IT needs so you can focus on growing your business. Get a free consultation today.'}
|
||||||
|
</p>
|
||||||
<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">
|
<Link
|
||||||
{data.h1}
|
to="/contact"
|
||||||
</h1>
|
className="inline-flex items-center gap-3 px-10 py-4 bg-blue-600 hover:bg-blue-700 text-white rounded-full font-bold text-lg transition-all hover:scale-105 shadow-xl hover:shadow-blue-500/25"
|
||||||
|
>
|
||||||
{/* Meta Info */}
|
Get Started
|
||||||
<div className="hero-stagger flex items-center gap-6 text-gray-600 dark:text-gray-400 mb-8 justify-center">
|
<span className="material-symbols-outlined">arrow_forward</span>
|
||||||
<div className="flex items-center gap-2">
|
</Link>
|
||||||
<span className="material-symbols-outlined text-sm">schedule</span>
|
</div>
|
||||||
<span>5 min read</span>
|
</motion.div>
|
||||||
</div>
|
</section>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="material-symbols-outlined text-sm">calendar_today</span>
|
{/* Related Content Grid */}
|
||||||
<span>January 2025</span>
|
<section className="px-6 py-16">
|
||||||
</div>
|
<div className="max-w-5xl mx-auto">
|
||||||
</div>
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
{/* Featured Image */}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
{data.image && (
|
viewport={{ once: true }}
|
||||||
<motion.div
|
className="font-display text-4xl font-bold mb-12 text-center text-gray-900 dark:text-white"
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
>
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
Why Choose Bay Area IT?
|
||||||
transition={{ delay: 0.4, duration: 0.8 }}
|
</motion.h2>
|
||||||
className="hero-stagger rounded-2xl overflow-hidden border border-gray-200 dark:border-white/10 shadow-2xl mb-8 max-w-md mx-auto"
|
|
||||||
>
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
<img
|
<motion.div
|
||||||
src={data.image}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
alt={data.h1}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
className="w-full h-auto max-h-64 object-cover"
|
viewport={{ once: true }}
|
||||||
/>
|
transition={{ delay: 0.1 }}
|
||||||
</motion.div>
|
className="p-8 bg-white dark:bg-[#111] rounded-3xl border border-gray-100 dark:border-white/5 hover:shadow-xl transition-shadow"
|
||||||
)}
|
>
|
||||||
|
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-900/20 rounded-2xl flex items-center justify-center mb-6">
|
||||||
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-3xl">
|
||||||
<motion.a
|
verified_user
|
||||||
href="/contact"
|
</span>
|
||||||
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
|
</div>
|
||||||
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
|
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||||
whileTap={{ scale: 0.95 }}
|
Proven Expertise
|
||||||
>
|
</h3>
|
||||||
Get IT Support
|
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||||
</motion.a>
|
Years of experience serving businesses across the Coastal Bend with comprehensive IT solutions.
|
||||||
<motion.a
|
</p>
|
||||||
href="/it-support-corpus-christi"
|
</motion.div>
|
||||||
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" }}
|
<motion.div
|
||||||
whileTap={{ scale: 0.95 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
>
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
View All Services
|
viewport={{ once: true }}
|
||||||
</motion.a>
|
transition={{ delay: 0.2 }}
|
||||||
</div>
|
className="p-8 bg-white dark:bg-[#111] rounded-3xl border border-gray-100 dark:border-white/5 hover:shadow-xl transition-shadow"
|
||||||
</div>
|
>
|
||||||
</section>
|
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-900/20 rounded-2xl flex items-center justify-center mb-6">
|
||||||
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-3xl">
|
||||||
{/* Main Content Section */}
|
support_agent
|
||||||
<section className="px-6 py-16 relative">
|
</span>
|
||||||
<div className="max-w-4xl mx-auto">
|
</div>
|
||||||
<motion.div
|
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
24/7 Support
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
</h3>
|
||||||
viewport={{ once: true }}
|
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||||
transition={{ duration: 0.6 }}
|
Remote-first support and practical escalation to keep work moving when issues appear.
|
||||||
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"
|
</p>
|
||||||
>
|
</motion.div>
|
||||||
<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 }} />
|
<motion.div
|
||||||
</div>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
</motion.div>
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
</div>
|
viewport={{ once: true }}
|
||||||
</section>
|
transition={{ delay: 0.3 }}
|
||||||
|
className="p-8 bg-white dark:bg-[#111] rounded-3xl border border-gray-100 dark:border-white/5 hover:shadow-xl transition-shadow"
|
||||||
{/* CTA Section */}
|
>
|
||||||
<section className="px-6 py-16">
|
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-900/20 rounded-2xl flex items-center justify-center mb-6">
|
||||||
<motion.div
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-3xl">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
handshake
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
</span>
|
||||||
viewport={{ once: true }}
|
</div>
|
||||||
className="max-w-4xl mx-auto"
|
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||||
>
|
Local Partnership
|
||||||
<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">
|
</h3>
|
||||||
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-6 block">
|
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||||
{category === 'Local Services' ? 'location_on' : 'insights'}
|
A trusted local partner who understands your community and business needs.
|
||||||
</span>
|
</p>
|
||||||
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">
|
</motion.div>
|
||||||
{category === 'Local Services'
|
</div>
|
||||||
? 'Ready to Get IT Support in Your Area?'
|
</div>
|
||||||
: 'Need Expert IT Support for Your Business?'}
|
</section>
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-gray-700 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
|
{/* Services Section */}
|
||||||
{category === 'Local Services'
|
<Services preview={true} />
|
||||||
? '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.'}
|
{/* Areas We Serve & CTA */}
|
||||||
</p>
|
<AreasWeServe />
|
||||||
<Link
|
<CTA />
|
||||||
to="/contact"
|
</div>
|
||||||
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>
|
export default BlogPostPage;
|
||||||
</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;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import SEO from '../../components/SEO';
|
||||||
|
|
||||||
const ContactPage: React.FC = () => {
|
const ContactPage: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -13,7 +14,14 @@ const ContactPage: React.FC = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
<>
|
||||||
|
<SEO
|
||||||
|
title="Contact Bay Area IT | Free IT Assessment in Corpus Christi"
|
||||||
|
description="Talk to Bay Area IT about managed IT support, help desk coverage, business email, networking, and technology support across Corpus Christi and the Coastal Bend."
|
||||||
|
keywords={['contact Bay Area IT', 'Corpus Christi IT consultation', 'managed IT assessment']}
|
||||||
|
canonicalUrl="https://bayareait.services/contact"
|
||||||
|
/>
|
||||||
|
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
||||||
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
||||||
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
|
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
@@ -200,8 +208,9 @@ const ContactPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ContactPage;
|
export default ContactPage;
|
||||||
|
|||||||
@@ -1,92 +1,106 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Hero from '../../components/Hero';
|
import LoadingScreen from '../../components/LoadingScreen';
|
||||||
import Mission from '../../components/Mission';
|
import Hero from '../../components/Hero';
|
||||||
import Services from '../../components/Services';
|
import Mission from '../../components/Mission';
|
||||||
import Process from '../../components/Process';
|
import Services from '../../components/Services';
|
||||||
import Blog from '../../components/Blog';
|
import Process from '../../components/Process';
|
||||||
import Testimonials from '../../components/Testimonials';
|
import Blog from '../../components/Blog';
|
||||||
import CTA from '../../components/CTA';
|
import Testimonials from '../../components/Testimonials';
|
||||||
import SEO from '../../components/SEO';
|
import CTA from '../../components/CTA';
|
||||||
import FAQ from '../../components/FAQ';
|
import SEO from '../../components/SEO';
|
||||||
import AreasWeServe from '../../components/AreasWeServe';
|
import FAQ from '../../components/FAQ';
|
||||||
import { locationData } from '../data/seoData';
|
import AreasWeServe from '../../components/AreasWeServe';
|
||||||
|
import { locationData } from '../data/seoData';
|
||||||
const HomePage: React.FC = () => {
|
|
||||||
useEffect(() => {
|
const HomePage: React.FC = () => {
|
||||||
window.scrollTo(0, 0);
|
const [isLoading, setIsLoading] = useState(() => {
|
||||||
}, []);
|
if (typeof window !== 'undefined') {
|
||||||
|
return !sessionStorage.getItem('home_loaded');
|
||||||
// Enhanced LocalBusiness Schema per SEO plan
|
}
|
||||||
const schema = {
|
return false;
|
||||||
"@context": "https://schema.org",
|
});
|
||||||
"@type": "ITService",
|
|
||||||
"name": "Bay Area IT Services",
|
useEffect(() => {
|
||||||
"image": "https://bayarea-cc.com/logo.png",
|
window.scrollTo(0, 0);
|
||||||
"@id": "https://bayarea-cc.com",
|
|
||||||
"url": "https://bayarea-cc.com",
|
if (isLoading) {
|
||||||
"telephone": "+1-361-XXX-XXXX", // TODO: Replace with actual phone
|
sessionStorage.setItem('home_loaded', 'true');
|
||||||
"priceRange": "$$",
|
}
|
||||||
"address": {
|
}, [isLoading]);
|
||||||
"@type": "PostalAddress",
|
|
||||||
"streetAddress": "[YOUR STREET]", // TODO: Add actual address
|
// Enhanced LocalBusiness Schema per SEO plan
|
||||||
"addressLocality": "Corpus Christi",
|
const schema = {
|
||||||
"addressRegion": "TX",
|
"@context": "https://schema.org",
|
||||||
"postalCode": "[YOUR ZIP]", // TODO: Add actual ZIP
|
"@type": "ITService",
|
||||||
"addressCountry": "US"
|
"name": "Bay Area IT",
|
||||||
},
|
"image": "https://bayareait.services/logo.svg",
|
||||||
"geo": {
|
"@id": "https://bayareait.services",
|
||||||
"@type": "GeoCoordinates",
|
"url": "https://bayareait.services",
|
||||||
"latitude": 27.800583,
|
"telephone": "+1-361-765-8400",
|
||||||
"longitude": -97.39638
|
"priceRange": "$$",
|
||||||
},
|
"address": {
|
||||||
"areaServed": [
|
"@type": "PostalAddress",
|
||||||
{ "@type": "City", "name": "Corpus Christi" },
|
"streetAddress": "1001 Blucher St",
|
||||||
{ "@type": "City", "name": "Portland" },
|
"addressLocality": "Corpus Christi",
|
||||||
{ "@type": "City", "name": "Rockport" },
|
"addressRegion": "TX",
|
||||||
{ "@type": "City", "name": "Aransas Pass" },
|
"postalCode": "78401",
|
||||||
{ "@type": "City", "name": "Kingsville" }
|
"addressCountry": "US"
|
||||||
],
|
},
|
||||||
"serviceType": [
|
"geo": {
|
||||||
"IT Support",
|
"@type": "GeoCoordinates",
|
||||||
"Business IT Support",
|
"latitude": 27.800583,
|
||||||
"Outsourced IT Services",
|
"longitude": -97.39638
|
||||||
"Computer Network Support",
|
},
|
||||||
"Cyber Security"
|
"areaServed": [
|
||||||
],
|
{ "@type": "City", "name": "Corpus Christi" },
|
||||||
"openingHoursSpecification": {
|
{ "@type": "City", "name": "Portland" },
|
||||||
"@type": "OpeningHoursSpecification",
|
{ "@type": "City", "name": "Rockport" },
|
||||||
"dayOfWeek": [
|
{ "@type": "City", "name": "Aransas Pass" },
|
||||||
"Monday",
|
{ "@type": "City", "name": "Kingsville" }
|
||||||
"Tuesday",
|
],
|
||||||
"Wednesday",
|
"serviceType": [
|
||||||
"Thursday",
|
"IT Support",
|
||||||
"Friday"
|
"IT Help Desk",
|
||||||
],
|
"Computer Support",
|
||||||
"opens": "08:00",
|
"Outsourced IT Services",
|
||||||
"closes": "18:00"
|
"Computer Network Support",
|
||||||
}
|
"Cyber Security"
|
||||||
};
|
],
|
||||||
|
"openingHoursSpecification": {
|
||||||
return (
|
"@type": "OpeningHoursSpecification",
|
||||||
<>
|
"dayOfWeek": [
|
||||||
<SEO
|
"Monday",
|
||||||
title="IT Service & IT Support for Businesses in Corpus Christi, TX"
|
"Tuesday",
|
||||||
description="Reliable IT support and IT services for businesses in Corpus Christi, TX. Fast response, outsourced IT support & help desk solutions. Call now."
|
"Wednesday",
|
||||||
keywords={["IT Service", "IT Support", "Corpus Christi", "Business IT Support"]}
|
"Thursday",
|
||||||
canonicalUrl={window.location.href}
|
"Friday"
|
||||||
schema={schema}
|
],
|
||||||
/>
|
"opens": "08:00",
|
||||||
<Hero />
|
"closes": "18:00"
|
||||||
<Mission />
|
}
|
||||||
<Services preview={true} />
|
};
|
||||||
<Process />
|
|
||||||
<Blog />
|
return (
|
||||||
<Testimonials />
|
<>
|
||||||
<AreasWeServe />
|
{isLoading && <LoadingScreen onComplete={() => setIsLoading(false)} />}
|
||||||
<FAQ items={locationData[0].faq} />
|
<SEO
|
||||||
<CTA />
|
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", "IT Help Desk"]}
|
||||||
};
|
canonicalUrl="https://bayareait.services/"
|
||||||
|
schema={schema}
|
||||||
export default HomePage;
|
/>
|
||||||
|
<Hero />
|
||||||
|
<Mission />
|
||||||
|
<Services preview={true} />
|
||||||
|
<Process />
|
||||||
|
<Blog />
|
||||||
|
<Testimonials />
|
||||||
|
<AreasWeServe />
|
||||||
|
<FAQ items={locationData[0].faq} />
|
||||||
|
<CTA />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
|
|||||||
121
src/pages/LegalPage.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import SEO from '../../components/SEO';
|
||||||
|
|
||||||
|
interface LegalSection {
|
||||||
|
title: string;
|
||||||
|
body: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LegalPageProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
canonicalUrl: string;
|
||||||
|
eyebrow: string;
|
||||||
|
intro: string;
|
||||||
|
sections: LegalSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LegalPage: React.FC<LegalPageProps> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
canonicalUrl,
|
||||||
|
eyebrow,
|
||||||
|
intro,
|
||||||
|
sections,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
canonicalUrl={canonicalUrl}
|
||||||
|
keywords={['Bay Area IT legal', 'privacy policy', 'terms of service']}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-[720px] bg-[radial-gradient(ellipse_75%_50%_at_50%_-20%,rgba(255,255,255,0.08),rgba(255,255,255,0))] pointer-events-none" />
|
||||||
|
<div className="absolute top-32 left-1/2 -translate-x-1/2 w-[760px] h-[420px] bg-white/5 rounded-full blur-[120px] pointer-events-none" />
|
||||||
|
|
||||||
|
<section className="px-6 py-20 border-b border-white/10 relative z-10">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-xs uppercase tracking-[0.35em] text-gray-500 mb-6"
|
||||||
|
>
|
||||||
|
{eyebrow}
|
||||||
|
</motion.p>
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 18 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.05 }}
|
||||||
|
className="font-display text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 18 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="max-w-3xl text-lg md:text-xl leading-relaxed text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
{intro}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-6 py-16 md:py-20 relative z-10">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<motion.article
|
||||||
|
key={section.title}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: '-10%' }}
|
||||||
|
transition={{ delay: index * 0.04 }}
|
||||||
|
className="rounded-[2rem] border border-white/10 bg-white/5 backdrop-blur-sm p-8 md:p-10 shadow-[0_30px_80px_rgba(0,0,0,0.18)]"
|
||||||
|
>
|
||||||
|
<h2 className="font-display text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-5">
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-base md:text-lg leading-relaxed text-gray-600 dark:text-gray-300">
|
||||||
|
{section.body.map((paragraph, paragraphIndex) => {
|
||||||
|
const [before, after] = paragraph.split('info@bayareaaffiliates.com');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p key={`${section.title}-${paragraphIndex}`}>
|
||||||
|
{after !== undefined ? (
|
||||||
|
<>
|
||||||
|
{before}
|
||||||
|
<a
|
||||||
|
href="mailto:info@bayareaaffiliates.com"
|
||||||
|
className="text-gray-900 dark:text-white underline decoration-white/30 underline-offset-4 transition-opacity hover:opacity-75"
|
||||||
|
>
|
||||||
|
info@bayareaaffiliates.com
|
||||||
|
</a>
|
||||||
|
{after}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
paragraph
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegalPage;
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import React, { useEffect, useRef, useLayoutEffect } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
|
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
import SEO from '../../components/SEO';
|
import SEO from '../../components/SEO';
|
||||||
|
import Breadcrumb from '../../components/Breadcrumb';
|
||||||
import Services from '../../components/Services';
|
import Services from '../../components/Services';
|
||||||
import CTA from '../../components/CTA';
|
import CTA from '../../components/CTA';
|
||||||
import FAQ from '../../components/FAQ';
|
import FAQ from '../../components/FAQ';
|
||||||
import AreasWeServe from '../../components/AreasWeServe';
|
import AreasWeServe from '../../components/AreasWeServe';
|
||||||
import { LocationData } from '../data/seoData';
|
import { LocationData } from '../data/seoData';
|
||||||
|
import heroBg from '../assets/hero-bg.webp';
|
||||||
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
@@ -21,14 +23,43 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const mouseX = useMotionValue(0);
|
const mouseX = useMotionValue(0);
|
||||||
const mouseY = useMotionValue(0);
|
const mouseY = useMotionValue(0);
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
const [isInteractive, setIsInteractive] = useState(false);
|
||||||
|
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
|
||||||
|
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prefersReducedMotion || typeof window === 'undefined') {
|
||||||
|
setIsInteractive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
|
||||||
|
const updateState = () => setIsInteractive(mediaQuery.matches);
|
||||||
|
|
||||||
|
updateState();
|
||||||
|
|
||||||
|
if (typeof mediaQuery.addEventListener === 'function') {
|
||||||
|
mediaQuery.addEventListener('change', updateState);
|
||||||
|
return () => mediaQuery.removeEventListener('change', updateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addListener(updateState);
|
||||||
|
return () => mediaQuery.removeListener(updateState);
|
||||||
|
}, [prefersReducedMotion]);
|
||||||
|
|
||||||
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
||||||
|
if (!isInteractive) return;
|
||||||
const { left, top } = currentTarget.getBoundingClientRect();
|
const { left, top } = currentTarget.getBoundingClientRect();
|
||||||
mouseX.set(clientX - left);
|
mouseX.set(clientX - left);
|
||||||
mouseY.set(clientY - top + 75);
|
mouseY.set(clientY - top + 75);
|
||||||
};
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
if (!isInteractive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
// Parallax Background
|
// Parallax Background
|
||||||
if (parallaxWrapperRef.current) {
|
if (parallaxWrapperRef.current) {
|
||||||
@@ -52,20 +83,20 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
}, containerRef);
|
}, containerRef);
|
||||||
|
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, []);
|
}, [isInteractive]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "LocalBusiness",
|
"@type": "LocalBusiness",
|
||||||
"name": "Bay Area IT Services",
|
"name": "Bay Area IT",
|
||||||
"url": window.location.href,
|
"url": `https://bayareait.services/${data.slug}`,
|
||||||
"areaServed": {
|
"areaServed": {
|
||||||
"@type": "City",
|
"@type": "City",
|
||||||
"name": data.city
|
"name": data.city
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +106,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
title={data.title}
|
title={data.title}
|
||||||
description={data.description}
|
description={data.description}
|
||||||
keywords={data.keywords}
|
keywords={data.keywords}
|
||||||
canonicalUrl={window.location.href}
|
canonicalUrl={`https://bayareait.services/${data.slug}`}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -83,7 +114,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={isInteractive ? handleMouseMove : undefined}
|
||||||
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
|
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
|
||||||
>
|
>
|
||||||
{/* Parallax Background */}
|
{/* Parallax Background */}
|
||||||
@@ -91,21 +122,25 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
||||||
{/* Base Layer */}
|
{/* Base Layer */}
|
||||||
<img
|
<img
|
||||||
alt="Abstract dark technology background"
|
alt="Abstract dark technology background"
|
||||||
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
||||||
src="/src/assets/hero-bg.png"
|
src={heroBg}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="high"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Highlight Layer */}
|
{isInteractive && (
|
||||||
<motion.img
|
<motion.img
|
||||||
style={{
|
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
|
||||||
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
|
alt=""
|
||||||
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
|
aria-hidden="true"
|
||||||
}}
|
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
||||||
alt=""
|
src={heroBg}
|
||||||
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
loading="lazy"
|
||||||
src="/src/assets/hero-bg.png"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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-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 className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
|
||||||
@@ -142,10 +177,10 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
Get IT Support
|
Get IT Support
|
||||||
</motion.a>
|
</motion.a>
|
||||||
<motion.a
|
<motion.a
|
||||||
href="/it-support-corpus-christi"
|
href="/services"
|
||||||
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"
|
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" }}
|
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
View All Services
|
View All Services
|
||||||
</motion.a>
|
</motion.a>
|
||||||
@@ -156,6 +191,13 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
{/* Main Content Section */}
|
{/* Main Content Section */}
|
||||||
<section className="px-6 py-16 relative">
|
<section className="px-6 py-16 relative">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Breadcrumb items={[
|
||||||
|
{ label: 'Home', to: '/' },
|
||||||
|
{ label: 'Locations', to: '/locations' },
|
||||||
|
{ label: data.city },
|
||||||
|
]} />
|
||||||
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
@@ -190,7 +232,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
|
<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.
|
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.
|
Learn more about our <Link to="/locations/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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +242,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Services Section */}
|
{/* Services Section */}
|
||||||
<Services featuredIds={[10, 9, 11]} />
|
<Services featuredIds={[6, 4, 3]} />
|
||||||
|
|
||||||
{/* Areas We Serve & CTA */}
|
{/* Areas We Serve & CTA */}
|
||||||
<AreasWeServe />
|
<AreasWeServe />
|
||||||
|
|||||||
106
src/pages/LocationsPage.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import SEO from '../../components/SEO';
|
||||||
|
import CTA from '../../components/CTA';
|
||||||
|
import { locationData } from '../data/seoData';
|
||||||
|
|
||||||
|
const cityLocations = locationData.filter((loc) =>
|
||||||
|
loc.slug.startsWith('locations/it-support-'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LocationsPage: React.FC = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO
|
||||||
|
title="IT Support Service Areas - Corpus Christi & Coastal Bend, TX"
|
||||||
|
description="Bay Area IT provides IT support and IT services throughout the Coastal Bend. View all cities we serve in the Corpus Christi area."
|
||||||
|
keywords={['IT support Corpus Christi', 'IT services Coastal Bend', 'local IT support Texas']}
|
||||||
|
canonicalUrl="https://bayareait.services/locations"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-background-light dark:bg-background-dark pt-20">
|
||||||
|
<section className="py-20 px-6 border-b border-gray-200 dark:border-white/10">
|
||||||
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-3 block">
|
||||||
|
Service Areas
|
||||||
|
</span>
|
||||||
|
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white tracking-tight">
|
||||||
|
IT Support Across the{' '}
|
||||||
|
<span className="text-gray-400 dark:text-gray-500">Coastal Bend</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto leading-relaxed">
|
||||||
|
We provide professional IT support and managed IT services for businesses
|
||||||
|
throughout Corpus Christi and the surrounding area. Remote-first, on-site
|
||||||
|
when needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="py-20 px-6">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{cityLocations.map((loc, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={loc.slug}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.07 }}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`/${loc.slug}`}
|
||||||
|
className="group block p-8 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-2xl hover:border-gray-400 dark:hover:border-white/30 hover:shadow-xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="material-symbols-outlined text-3xl text-gray-400 dark:text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-300 transition-colors">
|
||||||
|
location_on
|
||||||
|
</span>
|
||||||
|
<span className="material-symbols-outlined text-sm text-gray-400 dark:text-gray-600 group-hover:text-gray-700 dark:group-hover:text-white group-hover:translate-x-1 transition-all duration-200">
|
||||||
|
arrow_forward
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{loc.city}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">
|
||||||
|
{loc.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="py-16 px-6 bg-gray-50 dark:bg-white/5 border-t border-gray-200 dark:border-white/10">
|
||||||
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
<h2 className="font-display text-2xl md:text-3xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||||
|
Based in Corpus Christi. Serving the Region.
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-8 leading-relaxed">
|
||||||
|
Our team is locally based with over 25 years supporting Coastal Bend businesses.
|
||||||
|
Most issues are resolved remotely for speed. On-site support is available across
|
||||||
|
all cities we serve.
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Not seeing your city?{' '}
|
||||||
|
<Link to="/contact" className="text-gray-900 dark:text-white font-medium underline hover:no-underline transition-all">
|
||||||
|
Contact us
|
||||||
|
</Link>{' '}
|
||||||
|
and we may still be able to help.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<CTA />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationsPage;
|
||||||
55
src/pages/PrivacyPolicyPage.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import LegalPage from './LegalPage';
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'Information We Collect',
|
||||||
|
body: [
|
||||||
|
'We may collect personal information such as your name, email address, phone number, and other relevant details when you contact us for our IT support services.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'How We Use Your Information',
|
||||||
|
body: [
|
||||||
|
'Your information is used to provide and improve our services, respond to your inquiries, and communicate important updates.',
|
||||||
|
'We do not sell or rent your personal information to third parties.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Data Security',
|
||||||
|
body: [
|
||||||
|
'We implement a variety of security measures to maintain the safety of your personal information. However, no method of transmission over the internet or electronic storage is 100% secure.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Your Consent',
|
||||||
|
body: [
|
||||||
|
'By using our website and services, you consent to this Privacy Policy.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Changes to This Privacy Policy',
|
||||||
|
body: [
|
||||||
|
'We reserve the right to update this Privacy Policy at any time. Any changes will be effective immediately upon posting on this page.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Contact Us',
|
||||||
|
body: [
|
||||||
|
'If you have any questions regarding this Privacy Policy, please contact us at info@bayareaaffiliates.com.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PrivacyPolicyPage: React.FC = () => (
|
||||||
|
<LegalPage
|
||||||
|
title="Privacy Policy"
|
||||||
|
description="Read the Bay Area Affiliates, Inc. Privacy Policy to understand how we collect, use, and protect your information."
|
||||||
|
canonicalUrl="https://bayareait.services/privacy-policy"
|
||||||
|
eyebrow="Bay Area Affiliates, Inc."
|
||||||
|
intro="At Bay Area Affiliates, Inc., we are committed to protecting your privacy. This page explains how we collect, use, disclose, and safeguard information when you visit our website and use our services."
|
||||||
|
sections={sections}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PrivacyPolicyPage;
|
||||||
@@ -1,232 +1,274 @@
|
|||||||
import React, { useEffect, useRef, useLayoutEffect } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
|
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
import SEO from '../../components/SEO';
|
import SEO from '../../components/SEO';
|
||||||
import Services from '../../components/Services';
|
import Breadcrumb from '../../components/Breadcrumb';
|
||||||
import CTA from '../../components/CTA';
|
import Services from '../../components/Services';
|
||||||
import FAQ from '../../components/FAQ';
|
import CTA from '../../components/CTA';
|
||||||
import AreasWeServe from '../../components/AreasWeServe';
|
import FAQ from '../../components/FAQ';
|
||||||
import { ServiceData } from '../data/seoData';
|
import AreasWeServe from '../../components/AreasWeServe';
|
||||||
|
import { ServiceData } from '../data/seoData';
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
import heroBg from '../assets/hero-bg.webp';
|
||||||
|
|
||||||
interface ServicePageProps {
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
data: ServiceData;
|
|
||||||
}
|
interface ServicePageProps {
|
||||||
|
data: ServiceData;
|
||||||
const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
|
}
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
|
||||||
const mouseX = useMotionValue(0);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const mouseY = useMotionValue(0);
|
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mouseX = useMotionValue(0);
|
||||||
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
const mouseY = useMotionValue(0);
|
||||||
const { left, top } = currentTarget.getBoundingClientRect();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
mouseX.set(clientX - left);
|
const [isInteractive, setIsInteractive] = useState(false);
|
||||||
mouseY.set(clientY - top + 75);
|
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
|
||||||
};
|
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
const ctx = gsap.context(() => {
|
if (prefersReducedMotion || typeof window === 'undefined') {
|
||||||
// Parallax Background
|
setIsInteractive(false);
|
||||||
if (parallaxWrapperRef.current) {
|
return;
|
||||||
gsap.to(parallaxWrapperRef.current, {
|
}
|
||||||
yPercent: 30,
|
|
||||||
ease: "none",
|
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
|
||||||
scrollTrigger: {
|
const updateState = () => setIsInteractive(mediaQuery.matches);
|
||||||
trigger: containerRef.current,
|
|
||||||
start: "top top",
|
updateState();
|
||||||
end: "bottom top",
|
|
||||||
scrub: true
|
if (typeof mediaQuery.addEventListener === 'function') {
|
||||||
}
|
mediaQuery.addEventListener('change', updateState);
|
||||||
});
|
return () => mediaQuery.removeEventListener('change', updateState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text Stagger Animation
|
mediaQuery.addListener(updateState);
|
||||||
gsap.fromTo(".hero-stagger",
|
return () => mediaQuery.removeListener(updateState);
|
||||||
{ y: 50, opacity: 0 },
|
}, [prefersReducedMotion]);
|
||||||
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
|
|
||||||
);
|
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
||||||
}, containerRef);
|
if (!isInteractive) return;
|
||||||
|
const { left, top } = currentTarget.getBoundingClientRect();
|
||||||
return () => ctx.revert();
|
mouseX.set(clientX - left);
|
||||||
}, []);
|
mouseY.set(clientY - top + 75);
|
||||||
|
};
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, 0);
|
useLayoutEffect(() => {
|
||||||
}, []);
|
if (!isInteractive) {
|
||||||
|
return;
|
||||||
return (
|
}
|
||||||
<>
|
|
||||||
<SEO
|
const ctx = gsap.context(() => {
|
||||||
title={data.title}
|
// Parallax Background
|
||||||
description={data.description}
|
if (parallaxWrapperRef.current) {
|
||||||
keywords={data.keywords}
|
gsap.to(parallaxWrapperRef.current, {
|
||||||
canonicalUrl={window.location.href}
|
yPercent: 30,
|
||||||
/>
|
ease: "none",
|
||||||
|
scrollTrigger: {
|
||||||
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
trigger: containerRef.current,
|
||||||
{/* Hero Section */}
|
start: "top top",
|
||||||
<section
|
end: "bottom top",
|
||||||
ref={containerRef}
|
scrub: true
|
||||||
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">
|
// Text Stagger Animation
|
||||||
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
gsap.fromTo(".hero-stagger",
|
||||||
{/* Base Layer */}
|
{ y: 50, opacity: 0 },
|
||||||
<img
|
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
|
||||||
alt="Abstract dark technology background"
|
);
|
||||||
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
}, containerRef);
|
||||||
src="/src/assets/hero-bg.png"
|
|
||||||
/>
|
return () => ctx.revert();
|
||||||
|
}, [isInteractive]);
|
||||||
{/* Highlight Layer */}
|
|
||||||
<motion.img
|
useEffect(() => {
|
||||||
style={{
|
window.scrollTo(0, 0);
|
||||||
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)`,
|
|
||||||
}}
|
return (
|
||||||
alt=""
|
<>
|
||||||
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
<SEO
|
||||||
src="/src/assets/hero-bg.png"
|
title={data.title}
|
||||||
/>
|
description={data.description}
|
||||||
</div>
|
keywords={data.keywords}
|
||||||
<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>
|
canonicalUrl={`https://bayareait.services/${data.slug}`}
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
|
/>
|
||||||
</div>
|
|
||||||
|
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
||||||
{/* Hero Content */}
|
{/* Hero Section */}
|
||||||
<div className="relative z-10 text-center max-w-4xl px-6">
|
<section
|
||||||
<div className="hero-stagger flex items-center justify-center gap-2 mb-4">
|
ref={containerRef}
|
||||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
onMouseMove={isInteractive ? handleMouseMove : undefined}
|
||||||
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">
|
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
|
||||||
Professional IT Services
|
>
|
||||||
</span>
|
{/* Parallax Background */}
|
||||||
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||||
</div>
|
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
|
||||||
|
{/* Base Layer */}
|
||||||
<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">
|
<img
|
||||||
{data.h1.split(' ').slice(0, -2).join(' ')}<br />
|
alt="Abstract dark technology background"
|
||||||
<span className="text-gray-500 dark:text-gray-500">
|
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
|
||||||
{data.h1.split(' ').slice(-2).join(' ')}
|
src={heroBg}
|
||||||
</span>
|
loading="eager"
|
||||||
</h1>
|
decoding="async"
|
||||||
|
fetchPriority="high"
|
||||||
<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>
|
{isInteractive && (
|
||||||
|
<motion.img
|
||||||
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
|
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
|
||||||
<motion.a
|
alt=""
|
||||||
href="/contact"
|
aria-hidden="true"
|
||||||
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
|
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
|
||||||
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
|
src={heroBg}
|
||||||
whileTap={{ scale: 0.95 }}
|
loading="lazy"
|
||||||
>
|
decoding="async"
|
||||||
Get Started
|
/>
|
||||||
</motion.a>
|
)}
|
||||||
<motion.a
|
</div>
|
||||||
href="/it-support-corpus-christi"
|
<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>
|
||||||
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"
|
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
|
||||||
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
|
</div>
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
{/* Hero Content */}
|
||||||
View All Services
|
<div className="relative z-10 text-center max-w-4xl px-6">
|
||||||
</motion.a>
|
<div className="hero-stagger flex items-center justify-center gap-2 mb-4">
|
||||||
</div>
|
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
||||||
</div>
|
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">
|
||||||
</section>
|
Professional IT Services
|
||||||
|
</span>
|
||||||
{/* Main Content Section */}
|
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
|
||||||
<section className="px-6 py-16 relative">
|
</div>
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<motion.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">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
{data.h1.split(' ').slice(0, -2).join(' ')}<br />
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<span className="text-gray-500 dark:text-gray-500">
|
||||||
viewport={{ once: true }}
|
{data.h1.split(' ').slice(-2).join(' ')}
|
||||||
transition={{ duration: 0.6 }}
|
</span>
|
||||||
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"
|
</h1>
|
||||||
>
|
|
||||||
<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">
|
<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">
|
||||||
<div dangerouslySetInnerHTML={{ __html: data.content }} />
|
{data.description}
|
||||||
</div>
|
</p>
|
||||||
</motion.div>
|
|
||||||
</div>
|
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
</section>
|
<motion.a
|
||||||
|
href="/contact"
|
||||||
{/* Feature Highlight Section */}
|
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
|
||||||
<section className="px-6 py-16">
|
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
|
||||||
<div className="max-w-6xl mx-auto grid md:grid-cols-3 gap-8">
|
whileTap={{ scale: 0.95 }}
|
||||||
<motion.div
|
>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
Get Started
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
</motion.a>
|
||||||
viewport={{ once: true }}
|
<motion.a
|
||||||
transition={{ delay: 0.1 }}
|
href="/services"
|
||||||
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
|
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" }}
|
||||||
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
|
whileTap={{ scale: 0.95 }}
|
||||||
speed
|
>
|
||||||
</span>
|
View All Services
|
||||||
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
</motion.a>
|
||||||
Fast Response
|
</div>
|
||||||
</h3>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
</section>
|
||||||
Quick resolution of IT issues to minimize downtime and keep your business running smoothly.
|
|
||||||
</p>
|
{/* Main Content Section */}
|
||||||
</motion.div>
|
<section className="px-6 py-16 relative">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
<motion.div
|
<div className="mb-6">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<Breadcrumb items={[
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
{ label: 'Home', to: '/' },
|
||||||
viewport={{ once: true }}
|
{ label: 'Services', to: '/services' },
|
||||||
transition={{ delay: 0.2 }}
|
{ label: data.h1 },
|
||||||
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
|
]} />
|
||||||
>
|
</div>
|
||||||
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
|
<motion.div
|
||||||
verified_user
|
initial={{ opacity: 0, y: 20 }}
|
||||||
</span>
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
viewport={{ once: true }}
|
||||||
Proactive Security
|
transition={{ duration: 0.6 }}
|
||||||
</h3>
|
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"
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
>
|
||||||
Advanced security measures and monitoring to protect your business from cyber threats.
|
<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">
|
||||||
</p>
|
<div dangerouslySetInnerHTML={{ __html: data.content }} />
|
||||||
</motion.div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
<motion.div
|
</div>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
</section>
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
{/* Feature Highlight Section */}
|
||||||
transition={{ delay: 0.3 }}
|
<section className="px-6 py-16">
|
||||||
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
|
<div className="max-w-6xl mx-auto grid md:grid-cols-3 gap-8">
|
||||||
>
|
<motion.div
|
||||||
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
support_agent
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
</span>
|
viewport={{ once: true }}
|
||||||
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
transition={{ delay: 0.1 }}
|
||||||
Expert Team
|
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
|
||||||
</h3>
|
>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
|
||||||
Experienced IT professionals dedicated to providing exceptional service and support.
|
speed
|
||||||
</p>
|
</span>
|
||||||
</motion.div>
|
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||||
</div>
|
Fast Response
|
||||||
</section>
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
{/* Services Section */}
|
Quick resolution of IT issues to minimize downtime and keep your business running smoothly.
|
||||||
<Services featuredIds={data.relatedServices} />
|
</p>
|
||||||
|
</motion.div>
|
||||||
{/* Areas We Serve & CTA */}
|
|
||||||
<AreasWeServe />
|
<motion.div
|
||||||
<FAQ items={data.faq} />
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<CTA />
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
</div>
|
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">
|
||||||
export default ServicePage;
|
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;
|
||||||
|
|||||||
@@ -1,136 +1,117 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { motion, AnimatePresence, useScroll, useTransform, useMotionValueEvent } from 'framer-motion';
|
import { motion, AnimatePresence, useScroll, useTransform, useMotionValueEvent } from 'framer-motion';
|
||||||
import Contact from '../../components/Contact';
|
import Contact from '../../components/Contact';
|
||||||
|
import SEO from '../../components/SEO';
|
||||||
|
|
||||||
const services = [
|
const services = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Windows 11 Transition',
|
title: 'Bay Area Email Services',
|
||||||
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
|
description: 'Our flagship service: enterprise cloud email with 99.99% uptime, premium deliverability, local Corpus Christi support, and 25 GB mailboxes for $5 per inbox.',
|
||||||
challenge: 'Running outdated operating systems leaves your business vulnerable to security threats and compatibility issues.',
|
challenge: 'Missed emails, spam-folder delivery, and server downtime quietly cost businesses leads, approvals, and customer trust.',
|
||||||
approach: 'We manage the entire migration process, from hardware compatibility checks to software deployment and user training.',
|
approach: 'We use a serverless AWS-powered email architecture with S3 buffering for inbound mail and Amazon SES for outbound reputation, then manage DNS and migration for you locally.',
|
||||||
deliverables: [
|
deliverables: [
|
||||||
'Hardware compatibility assessment',
|
'25 GB business mailbox for $5 per inbox',
|
||||||
'Windows 11 deployment and configuration',
|
'99.99% uptime backed by cloud architecture',
|
||||||
'Application compatibility testing',
|
'S3 email buffering so incoming mail is never lost',
|
||||||
'Security policy implementation',
|
'Amazon SES sending for strong inbox placement',
|
||||||
'User training sessions'
|
'DNS setup, migration, forwarding, and auto-replies'
|
||||||
],
|
],
|
||||||
needs: [
|
needs: [
|
||||||
'Current device inventory',
|
'Your domain access',
|
||||||
'Software list',
|
'Mailbox count and user list',
|
||||||
'User schedule for upgrades'
|
'Current email provider details'
|
||||||
],
|
],
|
||||||
icon: 'desktop_windows',
|
icon: 'mail',
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
|
image: '/assets/services/business-it.webp'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Web Services',
|
title: 'Shared Drive',
|
||||||
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
|
description: 'Setup and management of shared drive solutions so your team can store, access, and organize files reliably.',
|
||||||
challenge: 'A poor online presence can cost you customers and credibility in a digital-first world.',
|
challenge: 'Data growth requires scalable storage that is accessible yet secure from loss.',
|
||||||
approach: 'We build professional, responsive websites and manage your digital identity to attract and retain customers.',
|
approach: 'We deploy shared drive solutions that centralize your files and make them easy for your team to access securely.',
|
||||||
deliverables: [
|
deliverables: [
|
||||||
'Custom website design & development',
|
'Shared drive setup and configuration',
|
||||||
'Domain registration & management',
|
'Folder structure and permissions',
|
||||||
'Professional email setup (M365/Google)',
|
'User permission management',
|
||||||
'SEO optimization basics',
|
'Remote access configuration',
|
||||||
'Hosting & maintenance'
|
'Backup integration'
|
||||||
],
|
],
|
||||||
needs: [
|
needs: [
|
||||||
'Brand guidelines / Logo',
|
'Capacity requirements',
|
||||||
'Content & copy',
|
'Access patterns'
|
||||||
'Domain access (if existing)'
|
],
|
||||||
],
|
icon: 'storage',
|
||||||
icon: 'language',
|
image: ''
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 3,
|
||||||
id: 3,
|
title: 'Printer & Scanner Installation',
|
||||||
title: 'Performance Upgrades',
|
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
|
||||||
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
|
challenge: 'Printer connectivity issues are a leading cause of office support tickets and downtime.',
|
||||||
challenge: 'Slow computers kill productivity and frustrate employees, leading to wasted time.',
|
approach: 'We set up reliable printing environments with proper drivers, networking, and user access controls.',
|
||||||
approach: 'We breathe new life into existing hardware with cost-effective upgrades and optimizations.',
|
deliverables: [
|
||||||
deliverables: [
|
'Network printer setup',
|
||||||
'SSD installation & cloning',
|
'Scanner configuration (Scan-to-Email/Folder)',
|
||||||
'RAM upgrades',
|
'Print server management',
|
||||||
'System cleanup & optimization',
|
'One-click user deployment',
|
||||||
'Thermal paste replacement',
|
'Troubleshooting training'
|
||||||
'Benchmark reporting'
|
],
|
||||||
],
|
needs: [
|
||||||
needs: [
|
'Printer/Scanner hardware',
|
||||||
'Access to devices',
|
'Network access details'
|
||||||
'Data backup confirmation'
|
],
|
||||||
],
|
icon: 'print',
|
||||||
icon: 'speed',
|
image: ''
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 4,
|
||||||
id: 4,
|
title: 'Web Design',
|
||||||
title: 'Printer & Scanner Installation',
|
description: 'Professional website design backed by domain registration and DNS support, so your online presence looks credible and works reliably.',
|
||||||
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
|
challenge: 'If your site looks dated, loads poorly, or your domain setup is messy, customers lose trust before they ever contact you.',
|
||||||
challenge: 'Printer connectivity issues are a leading cause of office support tickets and downtime.',
|
approach: 'We build clean business websites and handle the domain and DNS layer correctly, so your site, forms, and connected services stay dependable.',
|
||||||
approach: 'We set up reliable printing environments with proper drivers, networking, and user access controls.',
|
deliverables: [
|
||||||
deliverables: [
|
'Custom website design & development',
|
||||||
'Network printer setup',
|
'Domain registration & management',
|
||||||
'Scanner configuration (Scan-to-Email/Folder)',
|
'DNS setup and support',
|
||||||
'Print server management',
|
'SEO optimization basics',
|
||||||
'One-click user deployment',
|
'Hosting & maintenance'
|
||||||
'Troubleshooting training'
|
],
|
||||||
],
|
needs: [
|
||||||
needs: [
|
'Brand guidelines / Logo',
|
||||||
'Printer/Scanner hardware',
|
'Content & copy',
|
||||||
'Network access details'
|
'Domain access (if existing)'
|
||||||
],
|
],
|
||||||
icon: 'print',
|
icon: 'language',
|
||||||
image: ''
|
image: '/assets/services/managed-it.webp'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: 'New/Refurbished Desktop Hardware',
|
title: 'New/Refurbished Desktop Hardware',
|
||||||
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
|
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
|
||||||
challenge: 'Sourcing the right hardware at the right price can be time-consuming and risky.',
|
challenge: 'Sourcing the right hardware at the right price can be time-consuming and risky.',
|
||||||
approach: 'We source high-quality new and refurbished equipment that meets your specs and budget, fully tested and ready to go.',
|
approach: 'We source high-quality new and refurbished equipment that meets your specs and budget, fully tested and ready to go.',
|
||||||
deliverables: [
|
deliverables: [
|
||||||
'Hardware procurement',
|
'Hardware procurement',
|
||||||
'Quality assurance testing',
|
'Quality assurance testing',
|
||||||
'Image deployment',
|
'Image deployment',
|
||||||
'Peripherals setup',
|
'Peripherals setup',
|
||||||
'Warranty management'
|
'Warranty management'
|
||||||
],
|
],
|
||||||
needs: [
|
needs: [
|
||||||
'Budget constraints',
|
'Budget constraints',
|
||||||
'Performance requirements'
|
'Performance requirements'
|
||||||
],
|
],
|
||||||
icon: 'computer',
|
icon: 'computer',
|
||||||
image: ''
|
image: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
title: 'VPN Setup',
|
title: 'Network Infrastructure Support',
|
||||||
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
|
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
|
||||||
challenge: 'Remote work requires secure access to internal resources without exposing your network to threats.',
|
challenge: 'A weak network backbone leads to slow speeds, dropped calls, and security holes.',
|
||||||
approach: 'We implement robust VPN solutions like WireGuard or OpenVPN for secure, encrypted remote connectivity.',
|
approach: 'We design and maintain enterprise-grade networks that handle your data traffic reliably and securely.',
|
||||||
deliverables: [
|
|
||||||
'VPN server configuration',
|
|
||||||
'Client software deployment',
|
|
||||||
'Access control lists',
|
|
||||||
'Connection testing',
|
|
||||||
'User guides'
|
|
||||||
],
|
|
||||||
needs: [
|
|
||||||
'Public IP / DNS details',
|
|
||||||
'User list'
|
|
||||||
],
|
|
||||||
icon: 'vpn_lock',
|
|
||||||
image: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: 'Network Infrastructure Support',
|
|
||||||
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
|
|
||||||
challenge: 'A weak network backbone leads to slow speeds, dropped calls, and security holes.',
|
|
||||||
approach: 'We design and maintain enterprise-grade networks that handle your data traffic reliably and securely.',
|
|
||||||
deliverables: [
|
deliverables: [
|
||||||
'Router & Switch configuration',
|
'Router & Switch configuration',
|
||||||
'VLAN segmentation',
|
'VLAN segmentation',
|
||||||
@@ -141,31 +122,31 @@ const services = [
|
|||||||
needs: [
|
needs: [
|
||||||
'Floor plans (for Wi-Fi)',
|
'Floor plans (for Wi-Fi)',
|
||||||
'ISP details'
|
'ISP details'
|
||||||
],
|
],
|
||||||
icon: 'lan',
|
icon: 'lan',
|
||||||
image: ''
|
image: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 7,
|
||||||
title: 'Network Attached Storage',
|
title: 'IT Help Desk',
|
||||||
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
|
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
|
||||||
challenge: 'Data growth requires scalable storage that is accessible yet secure from loss.',
|
challenge: 'When employees do not have a clear support channel, small technical issues quickly turn into company-wide delays.',
|
||||||
approach: 'We deploy NAS solutions that centralize your data with redundancy and easy access for your team.',
|
approach: 'We provide a structured help desk workflow that resolves day-to-day IT issues quickly and keeps your team productive.',
|
||||||
deliverables: [
|
deliverables: [
|
||||||
'NAS hardware selection & setup',
|
'Remote troubleshooting and issue resolution',
|
||||||
'RAID configuration',
|
'User account and access support',
|
||||||
'User permission management',
|
'Software and email assistance',
|
||||||
'Remote access configuration',
|
'Escalation path for on-site issues',
|
||||||
'Backup integration'
|
'Clear communication on ticket status'
|
||||||
],
|
],
|
||||||
needs: [
|
needs: [
|
||||||
'Capacity requirements',
|
'Primary support contacts',
|
||||||
'Access patterns'
|
'User and device overview'
|
||||||
],
|
],
|
||||||
icon: 'storage',
|
icon: 'support_agent',
|
||||||
image: ''
|
image: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const ServiceModal: React.FC<{ service: typeof services[0] | null; onClose: () => void }> = ({ service, onClose }) => {
|
const ServiceModal: React.FC<{ service: typeof services[0] | null; onClose: () => void }> = ({ service, onClose }) => {
|
||||||
if (!service) return null;
|
if (!service) return null;
|
||||||
@@ -311,23 +292,30 @@ const ServicesPage: React.FC = () => {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-20 min-h-screen bg-[#0a0a0a] relative overflow-x-hidden">
|
<>
|
||||||
|
<SEO
|
||||||
|
title="IT Services | Bay Area IT Support, Email, Networking and Web"
|
||||||
|
description="Explore Bay Area IT services for Corpus Christi businesses, including help desk support, business email, networking, hardware, web design, and day-to-day IT support."
|
||||||
|
keywords={['IT services Corpus Christi', 'help desk services', 'business email setup', 'network support']}
|
||||||
|
canonicalUrl="https://bayareait.services/services"
|
||||||
|
/>
|
||||||
|
<div className="pt-20 min-h-screen bg-[#0a0a0a] relative overflow-x-hidden">
|
||||||
{/* Gradient for Services Page */}
|
{/* Gradient for Services Page */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.15),rgba(255,255,255,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.15),rgba(255,255,255,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
||||||
|
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<section className="py-20 px-6 bg-transparent border-b border-white/5 relative z-10">
|
<section className="py-20 px-6 bg-transparent border-b border-white/5 relative z-10">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<span className="text-white/60 font-bold tracking-widest uppercase text-sm mb-3 block">Expertise</span>
|
<span className="text-white/60 font-bold tracking-widest uppercase text-sm mb-3 block">Expertise</span>
|
||||||
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
|
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
|
||||||
Complete IT solutions for <br /><span className="text-gray-500">your business</span>
|
Reliable business technology <br /><span className="text-gray-500">that keeps communication moving</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
||||||
From desktop support to enterprise infrastructure, we provide the technology foundation your business needs to thrive.
|
From enterprise-grade email to day-to-day infrastructure and web support, we help businesses stay reachable, credible, and operational.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Timeline Section */}
|
{/* Timeline Section */}
|
||||||
<section ref={containerRef} className="py-24 px-6 relative overflow-hidden bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]">
|
<section ref={containerRef} className="py-24 px-6 relative overflow-hidden bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]">
|
||||||
@@ -341,7 +329,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 w-px bg-white origin-top shadow-[0_0_40px_2px_rgba(255,255,255,1)] drop-shadow-[0_0_10px_rgba(255,255,255,1)] -translate-x-1/2 md:translate-x-0"
|
className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 w-px bg-white origin-top shadow-[0_0_40px_2px_rgba(255,255,255,1)] drop-shadow-[0_0_10px_rgba(255,255,255,1)] -translate-x-1/2 md:translate-x-0"
|
||||||
></motion.div>
|
></motion.div>
|
||||||
|
|
||||||
<div className="space-y-64 pb-64">
|
<div className="space-y-40 pb-40">
|
||||||
{services.map((service, index) => (
|
{services.map((service, index) => (
|
||||||
<div
|
<div
|
||||||
key={service.id}
|
key={service.id}
|
||||||
@@ -369,10 +357,10 @@ const ServicesPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Content Card */}
|
{/* Content Card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
whileInView={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||||
className={`md:w-1/2 ${index % 2 === 0 ? 'md:pr-24 pl-12' : 'md:pl-24 pl-12'}`}
|
className={`md:w-1/2 ${index % 2 === 0 ? 'md:pr-24 pl-12' : 'md:pl-24 pl-12'}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -408,8 +396,9 @@ const ServicesPage: React.FC = () => {
|
|||||||
<ServiceModal service={selectedService} onClose={() => setSelectedService(null)} />
|
<ServiceModal service={selectedService} onClose={() => setSelectedService(null)} />
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ServicesPage;
|
export default ServicesPage;
|
||||||
|
|||||||
69
src/pages/TermsOfServicePage.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import LegalPage from './LegalPage';
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'Service Description',
|
||||||
|
body: [
|
||||||
|
'Bay Area Affiliates, Inc. provides comprehensive IT support services, including hardware and software support, network infrastructure, server repair, remote support, web services, virtualization solutions, and more.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'User Responsibilities',
|
||||||
|
body: [
|
||||||
|
'Users are responsible for maintaining the confidentiality of their account information and for all activities that occur under their account.',
|
||||||
|
'Users agree to provide accurate and complete information when using our services.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Payment Terms',
|
||||||
|
body: [
|
||||||
|
'All services rendered by Bay Area Affiliates, Inc. are subject to payment terms agreed upon in the service contract. Late payments may incur additional fees.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Limitation of Liability',
|
||||||
|
body: [
|
||||||
|
'Bay Area Affiliates, Inc. shall not be liable for any indirect, incidental, or consequential damages arising from the use of our services.',
|
||||||
|
'Our total liability is limited to the amount paid for the services rendered.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Termination',
|
||||||
|
body: [
|
||||||
|
'Either party may terminate the service agreement at any time with written notice. Upon termination, users must cease using all services provided by Bay Area Affiliates, Inc.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Governing Law',
|
||||||
|
body: [
|
||||||
|
'These Terms of Service are governed by and construed in accordance with the laws of the State of Texas, without regard to its conflict of law principles.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Changes to Terms',
|
||||||
|
body: [
|
||||||
|
'Bay Area Affiliates, Inc. reserves the right to modify these Terms of Service at any time. Any changes will be effective immediately upon posting on our website.',
|
||||||
|
'Continued use of our services constitutes acceptance of the updated terms.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Contact Us',
|
||||||
|
body: [
|
||||||
|
'If you have any questions about these Terms of Service, please contact us at info@bayareaaffiliates.com.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const TermsOfServicePage: React.FC = () => (
|
||||||
|
<LegalPage
|
||||||
|
title="Terms of Service"
|
||||||
|
description="Review the Bay Area Affiliates, Inc. Terms of Service for the use of our website and IT support services."
|
||||||
|
canonicalUrl="https://bayareait.services/terms-of-service"
|
||||||
|
eyebrow="Bay Area Affiliates, Inc."
|
||||||
|
intro="By accessing our website and using our IT support services, you agree to comply with and be bound by the following Terms of Service."
|
||||||
|
sections={sections}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TermsOfServicePage;
|
||||||
154
src/routes/seoRoutes.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { lazy, useEffect, useState } from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import type { LocationData, ServiceData, BlogPostData } from '../data/seoData';
|
||||||
|
|
||||||
|
const LazyLocationPage = lazy(() => import('../pages/LocationPage'));
|
||||||
|
const LazyServicePage = lazy(() => import('../pages/ServicePage'));
|
||||||
|
const LazyBlogPostPage = lazy(() => import('../pages/BlogPostPage'));
|
||||||
|
|
||||||
|
type SeoRouteState<T> = {
|
||||||
|
data: T | null;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeFallback = (
|
||||||
|
<div className="min-h-[40vh] flex items-center justify-center px-6 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Loading page...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function createSeoRoute<T extends { slug: string }>(
|
||||||
|
loadData: () => Promise<{
|
||||||
|
locationData?: LocationData[];
|
||||||
|
serviceData?: ServiceData[];
|
||||||
|
blogPostData?: BlogPostData[];
|
||||||
|
}>,
|
||||||
|
selectData: (module: Awaited<ReturnType<typeof loadData>>, slug: string) => T | undefined,
|
||||||
|
Page: React.LazyExoticComponent<React.ComponentType<{ data: T }>>,
|
||||||
|
) {
|
||||||
|
return function SeoRoute({ slug }: { slug: string }) {
|
||||||
|
const [state, setState] = useState<SeoRouteState<T>>({
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
setState({ data: null, error: null });
|
||||||
|
|
||||||
|
loadData()
|
||||||
|
.then((module) => {
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
const nextData = selectData(module, slug);
|
||||||
|
setState({
|
||||||
|
data: nextData ?? null,
|
||||||
|
error: nextData ? null : `No SEO data found for ${slug}`,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
setState({
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to load route data',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (state.error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[40vh] flex items-center justify-center px-6 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm uppercase tracking-widest text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Route unavailable
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-gray-900 dark:text-white">{state.error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.data) {
|
||||||
|
return routeFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Page data={state.data} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSeoData = () => import('../data/seoData');
|
||||||
|
|
||||||
|
// Location pages — city IT support pages under /locations/
|
||||||
|
export const locationRoutes = [
|
||||||
|
{ path: '/locations/it-support-corpus-christi', slug: 'locations/it-support-corpus-christi' },
|
||||||
|
{ path: '/locations/it-support-portland-tx', slug: 'locations/it-support-portland-tx' },
|
||||||
|
{ path: '/locations/it-support-rockport-tx', slug: 'locations/it-support-rockport-tx' },
|
||||||
|
{ path: '/locations/it-support-aransas-pass-tx', slug: 'locations/it-support-aransas-pass-tx' },
|
||||||
|
{ path: '/locations/it-support-kingsville-tx', slug: 'locations/it-support-kingsville-tx' },
|
||||||
|
// Service+location hybrid pages (stored in locationData, rendered via LocationPage)
|
||||||
|
{ path: '/services/web-design-corpus-christi', slug: 'services/web-design-corpus-christi' },
|
||||||
|
{ path: '/services/business-email-corpus-christi', slug: 'services/business-email-corpus-christi' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Service pages — all under /services/
|
||||||
|
export const serviceRoutes = [
|
||||||
|
{ path: '/services/it-help-desk', slug: 'services/it-help-desk' },
|
||||||
|
{ path: '/services/computer-support', slug: 'services/computer-support' },
|
||||||
|
{ path: '/services/business-email-services', slug: 'services/business-email-services' },
|
||||||
|
{ path: '/services/domain-registration-dns-support', slug: 'services/domain-registration-dns-support' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Authority blog posts only — location posts redirect to their location pages
|
||||||
|
export const blogRoutes = [
|
||||||
|
{ path: '/blog/it-support-small-business-corpus-christi', slug: 'blog/it-support-small-business-corpus-christi' },
|
||||||
|
{ path: '/blog/outsourced-it-support-corpus-christi', slug: 'blog/outsourced-it-support-corpus-christi' },
|
||||||
|
{ path: '/blog/it-service-vs-inhouse-it', slug: 'blog/it-service-vs-inhouse-it' },
|
||||||
|
{ path: '/blog/common-it-problems-businesses-corpus-christi', slug: 'blog/common-it-problems-businesses-corpus-christi' },
|
||||||
|
{ path: '/blog/it-support-cost-corpus-christi', slug: 'blog/it-support-cost-corpus-christi' },
|
||||||
|
{ path: '/blog/business-email-vs-google-workspace-vs-microsoft-365', slug: 'blog/business-email-vs-google-workspace-vs-microsoft-365' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Old flat-URL redirects — 301-equivalent for any links pointing to old paths
|
||||||
|
export const legacyRedirects = [
|
||||||
|
{ from: '/it-support-corpus-christi', to: '/locations/it-support-corpus-christi' },
|
||||||
|
{ from: '/it-support-portland-tx', to: '/locations/it-support-portland-tx' },
|
||||||
|
{ from: '/it-support-rockport-tx', to: '/locations/it-support-rockport-tx' },
|
||||||
|
{ from: '/it-support-aransas-pass-tx', to: '/locations/it-support-aransas-pass-tx' },
|
||||||
|
{ from: '/it-support-kingsville-tx', to: '/locations/it-support-kingsville-tx' },
|
||||||
|
{ from: '/it-help-desk', to: '/services/it-help-desk' },
|
||||||
|
{ from: '/computer-support', to: '/services/computer-support' },
|
||||||
|
{ from: '/business-email-services', to: '/services/business-email-services' },
|
||||||
|
{ from: '/domain-registration-dns-support', to: '/services/domain-registration-dns-support' },
|
||||||
|
{ from: '/web-design-corpus-christi', to: '/services/web-design-corpus-christi' },
|
||||||
|
{ from: '/business-email-corpus-christi', to: '/services/business-email-corpus-christi' },
|
||||||
|
// Location blog posts → their canonical location pages
|
||||||
|
{ from: '/blog/it-support-corpus-christi-blog', to: '/locations/it-support-corpus-christi' },
|
||||||
|
{ from: '/blog/it-support-portland-tx-blog', to: '/locations/it-support-portland-tx' },
|
||||||
|
{ from: '/blog/it-support-rockport-tx-blog', to: '/locations/it-support-rockport-tx' },
|
||||||
|
{ from: '/blog/it-support-aransas-pass-blog', to: '/locations/it-support-aransas-pass-tx' },
|
||||||
|
{ from: '/blog/it-support-kingsville-tx-blog', to: '/locations/it-support-kingsville-tx' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LocationSeoRoute = createSeoRoute<LocationData>(
|
||||||
|
loadSeoData,
|
||||||
|
(module, slug) => module.locationData.find((item) => item.slug === slug),
|
||||||
|
LazyLocationPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ServiceSeoRoute = createSeoRoute<ServiceData>(
|
||||||
|
loadSeoData,
|
||||||
|
(module, slug) => module.serviceData.find((item) => item.slug === slug),
|
||||||
|
LazyServicePage,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BlogSeoRoute = createSeoRoute<BlogPostData>(
|
||||||
|
loadSeoData,
|
||||||
|
(module, slug) => module.blogPostData.find((item) => item.slug === slug),
|
||||||
|
LazyBlogPostPage,
|
||||||
|
);
|
||||||
38
tailwind.config.cjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const forms = require('@tailwindcss/forms');
|
||||||
|
const typography = require('@tailwindcss/typography');
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./index.html',
|
||||||
|
'./App.tsx',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#FFFFFF',
|
||||||
|
'background-light': '#F3F4F6',
|
||||||
|
'background-dark': '#0a0a0a',
|
||||||
|
'surface-dark': '#121212',
|
||||||
|
'text-muted': '#888888',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
display: ['"Space Grotesk"', 'sans-serif'],
|
||||||
|
sans: ['"Inter"', 'sans-serif'],
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
DEFAULT: '4px',
|
||||||
|
lg: '8px',
|
||||||
|
xl: '12px',
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
grain:
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.05'/%3E%3C/svg%3E\")",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [forms, typography],
|
||||||
|
};
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2022",
|
"ES2022",
|
||||||
"DOM",
|
"DOM",
|
||||||
"DOM.Iterable"
|
"DOM.Iterable"
|
||||||
],
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": [
|
"types": [
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./*"
|
"./*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,24 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(() => {
|
||||||
const env = loadEnv(mode, '.', '');
|
|
||||||
return {
|
return {
|
||||||
server: {
|
server: {
|
||||||
port: 3012,
|
port: 3012,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
hmr: {
|
hmr: {
|
||||||
clientPort: 3012,
|
clientPort: 3012,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
define: {
|
resolve: {
|
||||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
alias: {
|
||||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
'@': path.resolve(__dirname, '.'),
|
||||||
},
|
}
|
||||||
resolve: {
|
}
|
||||||
alias: {
|
};
|
||||||
'@': path.resolve(__dirname, '.'),
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||