Changes für lighthouse Branch
This commit is contained in:
117
src/App.tsx
117
src/App.tsx
@@ -2,8 +2,12 @@ import { Toaster } from "@/components/ui/toaster";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
|
||||
import { lazy, Suspense, useEffect } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { reportWebVitals, observePerformance } from "@/utils/reportWebVitals";
|
||||
import { pageTransition } from "@/utils/animations";
|
||||
import ExitIntentPopup from "@/components/ExitIntentPopup";
|
||||
|
||||
// Eager load critical pages for better initial performance
|
||||
import Index from "./pages/Index";
|
||||
@@ -26,42 +30,101 @@ const NetworkAttachedStorage = lazy(() => import("./pages/NetworkAttachedStorage
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// Loading fallback component
|
||||
// Loading fallback component with animation
|
||||
const PageLoader = () => (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background-deep">
|
||||
<motion.div
|
||||
className="min-h-screen flex items-center justify-center bg-background-deep"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-neon/30 border-t-neon rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-foreground-muted">Loading...</p>
|
||||
<motion.div
|
||||
className="w-16 h-16 border-4 border-neon/30 border-t-neon rounded-full mx-auto mb-4"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.p
|
||||
className="text-foreground-muted"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
Loading...
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// Animated page wrapper
|
||||
const AnimatedPage = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={{
|
||||
initial: pageTransition.initial,
|
||||
animate: pageTransition.animate,
|
||||
exit: pageTransition.exit
|
||||
}}
|
||||
transition={pageTransition.transition}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppContent = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize Web Vitals monitoring
|
||||
reportWebVitals();
|
||||
observePerformance();
|
||||
}, []);
|
||||
|
||||
// Scroll to top on route change
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExitIntentPopup />
|
||||
<AnimatePresence mode="wait">
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route path="/" element={<AnimatedPage><Index /></AnimatedPage>} />
|
||||
<Route path="/services" element={<AnimatedPage><Services /></AnimatedPage>} />
|
||||
<Route path="/about" element={<AnimatedPage><About /></AnimatedPage>} />
|
||||
<Route path="/blog" element={<AnimatedPage><Blog /></AnimatedPage>} />
|
||||
<Route path="/blog/:slug" element={<AnimatedPage><BlogPost /></AnimatedPage>} />
|
||||
<Route path="/contact" element={<AnimatedPage><Contact /></AnimatedPage>} />
|
||||
<Route path="/windows-11-transition" element={<AnimatedPage><Windows11Transition /></AnimatedPage>} />
|
||||
<Route path="/vpn-setup" element={<AnimatedPage><VpnSetup /></AnimatedPage>} />
|
||||
<Route path="/web-services" element={<AnimatedPage><WebServices /></AnimatedPage>} />
|
||||
<Route path="/performance-upgrades" element={<AnimatedPage><PerformanceUpgrades /></AnimatedPage>} />
|
||||
<Route path="/printer-scanner-installation" element={<AnimatedPage><PrinterScannerInstallation /></AnimatedPage>} />
|
||||
<Route path="/desktop-hardware" element={<AnimatedPage><DesktopHardware /></AnimatedPage>} />
|
||||
<Route path="/network-infrastructure" element={<AnimatedPage><NetworkInfrastructure /></AnimatedPage>} />
|
||||
<Route path="/network-attached-storage" element={<AnimatedPage><NetworkAttachedStorage /></AnimatedPage>} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<AnimatedPage><NotFound /></AnimatedPage>} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/services" element={<Services />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/blog/:slug" element={<BlogPost />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/windows-11-transition" element={<Windows11Transition />} />
|
||||
<Route path="/vpn-setup" element={<VpnSetup />} />
|
||||
<Route path="/web-services" element={<WebServices />} />
|
||||
<Route path="/performance-upgrades" element={<PerformanceUpgrades />} />
|
||||
<Route path="/printer-scanner-installation" element={<PrinterScannerInstallation />} />
|
||||
<Route path="/desktop-hardware" element={<DesktopHardware />} />
|
||||
<Route path="/network-infrastructure" element={<NetworkInfrastructure />} />
|
||||
<Route path="/network-attached-storage" element={<NetworkAttachedStorage />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
<AppContent />
|
||||
</BrowserRouter>
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
134
src/components/ExitIntentPopup.tsx
Normal file
134
src/components/ExitIntentPopup.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { X, Download } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const ExitIntentPopup = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [hasShown, setHasShown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if popup was already shown in this session
|
||||
const shown = sessionStorage.getItem('exitIntentShown');
|
||||
if (shown) {
|
||||
setHasShown(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseLeave = (e: MouseEvent) => {
|
||||
// Only trigger if mouse is leaving from top of viewport
|
||||
if (e.clientY <= 0 && !hasShown) {
|
||||
setIsVisible(true);
|
||||
setHasShown(true);
|
||||
sessionStorage.setItem('exitIntentShown', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
// Add delay before activating to avoid false triggers
|
||||
const timer = setTimeout(() => {
|
||||
document.addEventListener('mouseleave', handleMouseLeave);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
document.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [hasShown]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 animate-in fade-in duration-300"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
{/* Popup */}
|
||||
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 animate-in zoom-in duration-300">
|
||||
<div className="card-dark p-8 max-w-lg w-[90vw] relative">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute top-4 right-4 text-foreground-muted hover:text-foreground transition-colors"
|
||||
aria-label="Close popup"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-neon/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Download className="w-8 h-8 text-neon" />
|
||||
</div>
|
||||
|
||||
<h2 className="font-heading font-bold text-2xl sm:text-3xl text-foreground mb-4">
|
||||
Wait! Don't leave empty-handed
|
||||
</h2>
|
||||
|
||||
<p className="text-foreground-muted mb-6 text-lg">
|
||||
Download our <span className="text-neon font-semibold">free Windows 11 Migration Checklist</span> —
|
||||
a $299 value guide to help you prepare for the upgrade.
|
||||
</p>
|
||||
|
||||
{/* Key benefits */}
|
||||
<ul className="text-left space-y-2 mb-6 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>Hardware compatibility check (avoid costly mistakes)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>Step-by-step migration timeline</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>Security & compliance considerations</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>Rollback plan (just in case)</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
to="/contact?lead=windows11-checklist"
|
||||
onClick={handleClose}
|
||||
className="btn-neon w-full flex items-center justify-center space-x-2 mb-3"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
<span>Download Free Checklist</span>
|
||||
</Link>
|
||||
|
||||
<p className="text-xs text-foreground-muted">
|
||||
No spam. No credit card required. Instant access.
|
||||
</p>
|
||||
|
||||
{/* Close link */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-sm text-foreground-muted hover:text-foreground mt-4 underline"
|
||||
>
|
||||
No thanks, I'll figure it out myself
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExitIntentPopup;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { navVariants, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations';
|
||||
|
||||
const Navigation = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -27,87 +29,173 @@ const Navigation = () => {
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
return (
|
||||
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
||||
? 'bg-white/5 backdrop-blur-2xl border-b border-white/20 shadow-2xl shadow-black/20'
|
||||
: 'bg-transparent'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<>
|
||||
{/* Skip link for accessibility */}
|
||||
<a href="#main-content" className="skip-link">
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
<motion.nav
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
||||
? 'bg-white/5 backdrop-blur-2xl border-b border-white/20 shadow-2xl shadow-black/20'
|
||||
: 'bg-transparent'
|
||||
}`}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={navVariants}
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-14 md:h-16">
|
||||
{/* Logo with animation */}
|
||||
<Link to="/" className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 flex items-center justify-center overflow-visible">
|
||||
<motion.div
|
||||
className="w-12 h-12 flex items-center justify-center overflow-visible"
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.6, ease: "easeInOut" }}
|
||||
>
|
||||
<img
|
||||
src="/logo_bayarea.svg"
|
||||
alt="Bay Area Affiliates Logo"
|
||||
className="w-10 h-10 opacity-90 hover:opacity-100 transition-opacity duration-300"
|
||||
className="w-10 h-10 opacity-90"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
<span className="font-heading font-bold text-lg text-white">
|
||||
Bay Area Affiliates
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
{/* Desktop Navigation with animations */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
{navItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className={`font-medium transition-colors duration-200 ${isActive(item.path)
|
||||
? 'text-neon'
|
||||
: 'text-white hover:text-neon'
|
||||
}`}
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
to="/contact"
|
||||
className="btn-neon ml-4"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="md:hidden text-white hover:text-neon transition-colors"
|
||||
aria-label="Toggle navigation menu"
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{isOpen && (
|
||||
<div className="md:hidden bg-white/5 backdrop-blur-2xl border-t border-white/20">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${isActive(item.path)
|
||||
? 'text-neon bg-neon/10'
|
||||
: 'text-white hover:text-neon hover:bg-neon/5'
|
||||
className={`font-medium transition-all duration-200 relative group px-2 py-1 ${isActive(item.path)
|
||||
? 'text-neon'
|
||||
: 'text-white hover:text-neon'
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
{/* Animated underline */}
|
||||
<motion.span
|
||||
className="absolute -bottom-1 left-0 h-0.5 bg-neon rounded-full"
|
||||
initial={{ width: isActive(item.path) ? '100%' : 0 }}
|
||||
whileHover={{ width: '100%', boxShadow: '0 0 8px rgba(51, 102, 255, 0.6)' }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
/>
|
||||
{/* Glow effect on hover */}
|
||||
<motion.span
|
||||
className="absolute inset-0 bg-neon/5 rounded-lg -z-10"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileHover={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5, duration: 0.5 }}
|
||||
whileHover={buttonHover}
|
||||
whileTap={buttonTap}
|
||||
>
|
||||
<Link
|
||||
to="/contact"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="block w-full text-center btn-neon mt-4"
|
||||
className="btn-neon ml-4"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile menu button with animation */}
|
||||
<motion.button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="md:hidden text-white hover:text-neon transition-colors"
|
||||
aria-label="Toggle navigation menu"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{isOpen ? (
|
||||
<motion.div
|
||||
key="close"
|
||||
initial={{ rotate: -90, opacity: 0 }}
|
||||
animate={{ rotate: 0, opacity: 1 }}
|
||||
exit={{ rotate: 90, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<X size={24} />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="menu"
|
||||
initial={{ rotate: 90, opacity: 0 }}
|
||||
animate={{ rotate: 0, opacity: 1 }}
|
||||
exit={{ rotate: -90, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Menu size={24} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation with smooth animations */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="md:hidden bg-white/5 backdrop-blur-2xl border-t border-white/20 overflow-hidden"
|
||||
>
|
||||
<motion.div
|
||||
className="px-2 pt-2 pb-3 space-y-1"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<motion.div key={item.name} variants={staggerItem}>
|
||||
<Link
|
||||
to={item.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${isActive(item.path)
|
||||
? 'text-neon bg-neon/10'
|
||||
: 'text-white hover:text-neon hover:bg-neon/5'
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
<motion.div variants={staggerItem}>
|
||||
<Link
|
||||
to="/contact"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="block w-full text-center btn-neon mt-4"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</nav>
|
||||
</motion.nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ReactNode, useLayoutEffect, useRef } from 'react';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
import { ReactNode } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { scrollRevealVariants } from '@/utils/animations';
|
||||
|
||||
type ScrollRevealProps = {
|
||||
children: ReactNode;
|
||||
@@ -11,38 +10,20 @@ type ScrollRevealProps = {
|
||||
};
|
||||
|
||||
const ScrollReveal = ({ children, delay = 0, className = '' }: ScrollRevealProps) => {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.fromTo(
|
||||
element,
|
||||
{ autoAlpha: 0, y: 32 },
|
||||
{
|
||||
autoAlpha: 1,
|
||||
y: 0,
|
||||
duration: 0.8,
|
||||
ease: 'power3.out',
|
||||
delay: delay / 1000,
|
||||
scrollTrigger: {
|
||||
trigger: element,
|
||||
start: 'top 85%',
|
||||
once: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
}, element);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [delay]);
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-100px" });
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={className}>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
variants={scrollRevealVariants}
|
||||
transition={{ delay: delay / 1000 }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
200
src/components/home/BackgroundAnimations.tsx
Normal file
200
src/components/home/BackgroundAnimations.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const BackgroundAnimations = () => {
|
||||
// Reduced particles for better performance (30 -> 12)
|
||||
const particles = useMemo(() => {
|
||||
return Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
size: Math.random() * 2 + 1,
|
||||
duration: Math.random() * 12 + 8,
|
||||
delay: Math.random() * 4,
|
||||
opacity: Math.random() * 0.5 + 0.2,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reduced circuit nodes for performance (12 -> 6)
|
||||
const circuitNodes = useMemo(() => {
|
||||
return Array.from({ length: 6 }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 80 + 10,
|
||||
y: Math.random() * 80 + 10,
|
||||
pulseDelay: Math.random() * 2,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reduced connection lines (half as many)
|
||||
const connectionLines = useMemo(() => {
|
||||
const lines = [];
|
||||
for (let i = 0; i < circuitNodes.length - 1; i += 2) {
|
||||
if (i + 1 < circuitNodes.length) {
|
||||
lines.push({
|
||||
id: i,
|
||||
x1: circuitNodes[i].x,
|
||||
y1: circuitNodes[i].y,
|
||||
x2: circuitNodes[i + 1].x,
|
||||
y2: circuitNodes[i + 1].y,
|
||||
});
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}, [circuitNodes]);
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none" style={{ willChange: 'transform' }}>
|
||||
{/* Simplified Static Grid Background - no animation for better performance */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, rgba(51, 102, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(51, 102, 255, 0.03) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '80px 80px',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* SVG Container for Lines and Nodes - optimized */}
|
||||
<svg className="absolute inset-0 w-full h-full" style={{ opacity: 0.5, willChange: 'transform' }}>
|
||||
|
||||
{/* Simplified Connection Lines - static for better performance */}
|
||||
{connectionLines.map((line) => (
|
||||
<line
|
||||
key={line.id}
|
||||
x1={`${line.x1}%`}
|
||||
y1={`${line.y1}%`}
|
||||
x2={`${line.x2}%`}
|
||||
y2={`${line.y2}%`}
|
||||
stroke="rgba(51, 102, 255, 0.2)"
|
||||
strokeWidth="1"
|
||||
opacity="0.4"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Reduced Vertical Data Streams (8 -> 4) */}
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<motion.line
|
||||
key={`vertical-${i}`}
|
||||
x1={`${15 + i * 25}%`}
|
||||
y1="0%"
|
||||
x2={`${15 + i * 25}%`}
|
||||
y2="100%"
|
||||
stroke="rgba(51, 102, 255, 0.15)"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="10 20"
|
||||
initial={{ strokeDashoffset: 0 }}
|
||||
animate={{ strokeDashoffset: 100 }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
delay: i * 0.5,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Simplified Circuit Nodes - reduced animation */}
|
||||
{circuitNodes.map((node) => (
|
||||
<g key={node.id}>
|
||||
{/* Core node with simple pulse */}
|
||||
<motion.circle
|
||||
cx={`${node.x}%`}
|
||||
cy={`${node.y}%`}
|
||||
r="2.5"
|
||||
fill="#3366ff"
|
||||
animate={{
|
||||
opacity: [0.6, 1, 0.6],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
delay: node.pulseDelay,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Optimized Floating Data Particles with GPU acceleration */}
|
||||
{particles.map((particle) => (
|
||||
<motion.div
|
||||
key={particle.id}
|
||||
className="absolute rounded-full bg-neon"
|
||||
style={{
|
||||
width: `${particle.size}px`,
|
||||
height: `${particle.size}px`,
|
||||
left: `${particle.x}%`,
|
||||
top: `${particle.y}%`,
|
||||
opacity: particle.opacity,
|
||||
boxShadow: '0 0 6px rgba(51, 102, 255, 0.6)',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -600],
|
||||
opacity: [0, particle.opacity, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: particle.duration,
|
||||
repeat: Infinity,
|
||||
delay: particle.delay,
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Simplified Static Scanline Effect - no animation */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 pointer-events-none"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(51, 102, 255, 0.02) 2px,
|
||||
rgba(51, 102, 255, 0.02) 4px
|
||||
)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Reduced Radial Glows (3 -> 2) with simpler animation */}
|
||||
{[
|
||||
{ x: 25, y: 40 },
|
||||
{ x: 75, y: 60 },
|
||||
].map((pos, i) => (
|
||||
<motion.div
|
||||
key={`glow-${i}`}
|
||||
className="absolute rounded-full pointer-events-none"
|
||||
style={{
|
||||
left: `${pos.x}%`,
|
||||
top: `${pos.y}%`,
|
||||
width: '180px',
|
||||
height: '180px',
|
||||
background: 'radial-gradient(circle, rgba(51, 102, 255, 0.12) 0%, transparent 70%)',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.15, 1],
|
||||
opacity: [0.4, 0.6, 0.4],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: i * 2,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Static Corner Accent Glows - no blur for better performance */}
|
||||
<div className="absolute top-0 left-0 w-64 h-64 bg-gradient-to-br from-neon/8 to-transparent rounded-full opacity-40 pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-0 w-80 h-80 bg-gradient-to-tl from-neon/8 to-transparent rounded-full opacity-40 pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackgroundAnimations;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ArrowRight, Clock, DollarSign, Phone } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ScrollReveal from '../ScrollReveal';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { fadeInUp, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations';
|
||||
|
||||
const CTASection = () => {
|
||||
const faqs = [
|
||||
@@ -21,6 +23,13 @@ const CTASection = () => {
|
||||
}
|
||||
];
|
||||
|
||||
const headerRef = useRef(null);
|
||||
const ctaRef = useRef(null);
|
||||
const faqRef = useRef(null);
|
||||
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
|
||||
const isCtaInView = useInView(ctaRef, { once: true, margin: "-100px" });
|
||||
const isFaqInView = useInView(faqRef, { once: true, margin: "-100px" });
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-background-deep relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
@@ -29,88 +38,141 @@ const CTASection = () => {
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<ScrollReveal>
|
||||
<motion.div
|
||||
ref={headerRef}
|
||||
initial="hidden"
|
||||
animate={isHeaderInView ? "visible" : "hidden"}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl lg:text-6xl text-foreground mb-6">
|
||||
Ready for <span className="text-neon text-glow">reliable IT?</span>
|
||||
Ready for <motion.span
|
||||
className="text-neon text-glow"
|
||||
animate={{
|
||||
textShadow: [
|
||||
"0 0 20px rgba(51, 102, 255, 0.5)",
|
||||
"0 0 40px rgba(51, 102, 255, 0.8)",
|
||||
"0 0 20px rgba(51, 102, 255, 0.5)"
|
||||
]
|
||||
}}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
reliable IT?
|
||||
</motion.span>
|
||||
</h2>
|
||||
|
||||
|
||||
<p className="text-xl text-foreground-muted mb-12 leading-relaxed">
|
||||
Join 150+ Coastal Bend businesses that trust us with their technology.
|
||||
Join 150+ Coastal Bend businesses that trust us with their technology.
|
||||
Get started with a free 20-minute assessment.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
|
||||
{/* Primary CTAs */}
|
||||
<ScrollReveal delay={200}>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16">
|
||||
<motion.div
|
||||
ref={ctaRef}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isCtaInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ delay: 0.2, duration: 0.6 }}
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16"
|
||||
>
|
||||
<motion.div whileHover={buttonHover} whileTap={buttonTap}>
|
||||
<Link
|
||||
to="/contact"
|
||||
className="btn-neon group flex items-center space-x-2 text-lg px-8 py-4"
|
||||
>
|
||||
<span>Book a 20-minute assessment</span>
|
||||
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
|
||||
<motion.div
|
||||
animate={{ x: [0, 3, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
</motion.div>
|
||||
|
||||
<motion.div whileHover={buttonHover} whileTap={buttonTap}>
|
||||
<Link
|
||||
to="/contact"
|
||||
className="btn-ghost group flex items-center space-x-2 text-lg px-8 py-4"
|
||||
>
|
||||
<span>Send a message</span>
|
||||
</Link>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Micro FAQ */}
|
||||
<ScrollReveal delay={400}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{faqs.map((faq, index) => {
|
||||
const Icon = faq.icon;
|
||||
|
||||
return (
|
||||
<div key={faq.question} className="text-left">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 bg-neon/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<Icon className="w-4 h-4 text-neon" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground mb-2">
|
||||
{faq.question}
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-muted">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</div>
|
||||
<motion.div
|
||||
ref={faqRef}
|
||||
initial="hidden"
|
||||
animate={isFaqInView ? "visible" : "hidden"}
|
||||
variants={staggerContainer}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
||||
>
|
||||
{faqs.map((faq, index) => {
|
||||
const Icon = faq.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={faq.question}
|
||||
variants={staggerItem}
|
||||
className="text-left"
|
||||
>
|
||||
<motion.div
|
||||
className="flex items-start space-x-3"
|
||||
whileHover={{ x: 5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="w-8 h-8 bg-neon/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"
|
||||
whileHover={{ rotate: 360, scale: 1.1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Icon className="w-4 h-4 text-neon" />
|
||||
</motion.div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground mb-2">
|
||||
{faq.question}
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-muted">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
|
||||
{/* Contact info */}
|
||||
<ScrollReveal delay={600}>
|
||||
<div className="mt-16 pt-8 border-t border-border/30">
|
||||
<p className="text-sm text-foreground-muted mb-4">
|
||||
Ready to talk? We're here to help.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center text-sm">
|
||||
<a
|
||||
href="tel:+1-361-555-0123"
|
||||
className="text-neon hover:text-neon/80 transition-colors flex items-center"
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
(361) 555-0123
|
||||
</a>
|
||||
<span className="hidden sm:block text-border">|</span>
|
||||
<a
|
||||
href="mailto:info@bayareaaffiliates.com"
|
||||
className="text-neon hover:text-neon/80 transition-colors"
|
||||
>
|
||||
info@bayareaaffiliates.com
|
||||
</a>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isFaqInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ delay: 0.6, duration: 0.6 }}
|
||||
className="mt-16 pt-8 border-t border-border/30"
|
||||
>
|
||||
<p className="text-sm text-foreground-muted mb-4">
|
||||
Ready to talk? We're here to help.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center text-sm">
|
||||
<motion.a
|
||||
href="tel:+1-361-555-0123"
|
||||
className="text-neon hover:text-neon/80 transition-colors flex items-center"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
(361) 555-0123
|
||||
</motion.a>
|
||||
<span className="hidden sm:block text-border">|</span>
|
||||
<motion.a
|
||||
href="mailto:info@bayareaaffiliates.com"
|
||||
className="text-neon hover:text-neon/80 transition-colors"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
info@bayareaaffiliates.com
|
||||
</motion.a>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,87 +1,165 @@
|
||||
import { ArrowRight, Play } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import heroNetwork from '@/assets/hero-network.jpg';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import { heroVariants, heroItemVariants, buttonHover, buttonTap, easing } from '@/utils/animations';
|
||||
import BackgroundAnimations from './BackgroundAnimations';
|
||||
|
||||
const HeroSection = () => {
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const { scrollY } = useScroll();
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (imageRef.current) {
|
||||
const scrolled = window.pageYOffset;
|
||||
const parallax = scrolled * 0.5;
|
||||
imageRef.current.style.transform = `translateY(${parallax}px) scale(1.1)`;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
// Smooth parallax effect with Framer Motion
|
||||
const y = useTransform(scrollY, [0, 500], [0, 150]);
|
||||
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
|
||||
|
||||
return (
|
||||
<section className="section-pin">
|
||||
<div className="relative h-full flex items-center justify-center overflow-hidden">
|
||||
{/* Background image with parallax */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<img
|
||||
ref={imageRef}
|
||||
src="/serverroom.png"
|
||||
alt="Modern IT infrastructure with network connections"
|
||||
className="w-full h-[110%] object-cover will-change-transform"
|
||||
style={{ transform: 'translateY(0px) scale(1.1)' }}
|
||||
/>
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-black/35"></div>
|
||||
</div>
|
||||
{/* Background image with smooth parallax */}
|
||||
<motion.div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ y }}
|
||||
>
|
||||
<picture>
|
||||
<source type="image/avif" srcSet="/serverroom.avif" />
|
||||
<source type="image/webp" srcSet="/serverroom.webp" />
|
||||
<motion.img
|
||||
src="/serverroom.png"
|
||||
alt="Modern IT infrastructure with network connections and server equipment"
|
||||
className="w-full h-[110%] object-cover"
|
||||
initial={{ scale: 1.1, opacity: 0 }}
|
||||
animate={{ scale: 1.1, opacity: 1 }}
|
||||
transition={{ duration: 1.2, ease: easing.elegant }}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
{/* Animated background effects */}
|
||||
<BackgroundAnimations />
|
||||
|
||||
{/* Darker overlay for better text contrast */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/50 to-black/60"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* Main content with staggered animations */}
|
||||
<motion.div
|
||||
className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"
|
||||
variants={heroVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
style={{ opacity }}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full bg-neon/20 border border-neon/40 text-neon text-sm font-medium mb-8 drop-shadow-[0_0_10px_rgba(51,102,255,0.5)]">
|
||||
<span className="w-2 h-2 bg-neon rounded-full mr-2 animate-glow-pulse"></span>
|
||||
<motion.div
|
||||
variants={heroItemVariants}
|
||||
className="inline-flex items-center px-4 py-2 rounded-full bg-neon/20 border border-neon/40 text-neon text-sm font-medium mb-8 drop-shadow-[0_0_10px_rgba(51,102,255,0.5)]"
|
||||
>
|
||||
<motion.span
|
||||
className="w-2 h-2 bg-neon rounded-full mr-2"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [1, 0.7, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
Serving the Coastal Bend since 2010
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Main headline */}
|
||||
<h1 className="font-heading font-bold text-5xl sm:text-6xl lg:text-7xl text-white mb-6 text-balance drop-shadow-[0_0_20px_rgba(255,255,255,0.3)]">
|
||||
<motion.h1
|
||||
variants={heroItemVariants}
|
||||
className="font-heading font-bold text-5xl sm:text-6xl lg:text-7xl text-white mb-6 text-balance drop-shadow-[0_0_20px_rgba(255,255,255,0.3)]"
|
||||
>
|
||||
Modern IT that keeps your{' '}
|
||||
<span className="text-neon text-glow drop-shadow-[0_0_30px_rgba(51,102,255,0.8)]">business moving</span>
|
||||
</h1>
|
||||
<motion.span
|
||||
className="text-neon text-glow drop-shadow-[0_0_30px_rgba(51,102,255,0.8)]"
|
||||
animate={{
|
||||
textShadow: [
|
||||
'0 0 20px rgba(51,102,255,0.5)',
|
||||
'0 0 30px rgba(51,102,255,0.8)',
|
||||
'0 0 20px rgba(51,102,255,0.5)',
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
business moving
|
||||
</motion.span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Subheadline */}
|
||||
<p className="text-xl sm:text-2xl text-white/95 mb-12 max-w-3xl mx-auto leading-relaxed drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]">
|
||||
<motion.p
|
||||
variants={heroItemVariants}
|
||||
className="text-xl sm:text-2xl text-white/95 mb-12 max-w-3xl mx-auto leading-relaxed drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]"
|
||||
>
|
||||
From fast devices to secure remote access and resilient networks — we design,
|
||||
run and protect your tech so you can focus on growth.
|
||||
</p>
|
||||
</motion.p>
|
||||
|
||||
{/* CTA buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Link
|
||||
to="/contact"
|
||||
className="btn-neon group flex items-center space-x-2"
|
||||
{/* CTA buttons with hover animations */}
|
||||
<motion.div
|
||||
variants={heroItemVariants}
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={buttonHover}
|
||||
whileTap={buttonTap}
|
||||
>
|
||||
<span>Get in touch</span>
|
||||
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/contact"
|
||||
className="btn-neon group flex items-center space-x-2"
|
||||
>
|
||||
<span>Get in touch</span>
|
||||
<motion.div
|
||||
animate={{ x: [0, 3, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</motion.div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<button className="btn-ghost group flex items-center space-x-2">
|
||||
<Play className="w-5 h-5 transition-transform group-hover:scale-110" />
|
||||
<span>See our system</span>
|
||||
</button>
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={buttonHover}
|
||||
whileTap={buttonTap}
|
||||
>
|
||||
<Link
|
||||
to="/services"
|
||||
className="btn-ghost group flex items-center space-x-2"
|
||||
>
|
||||
<Play className="w-5 h-5" />
|
||||
<span>See our services</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2">
|
||||
{/* Animated scroll indicator */}
|
||||
<motion.div
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.5, duration: 0.8 }}
|
||||
>
|
||||
<div className="w-6 h-10 border-2 border-neon/60 rounded-full flex justify-center drop-shadow-[0_0_10px_rgba(51,102,255,0.3)]">
|
||||
<div className="w-1 h-3 bg-neon rounded-full mt-2 animate-bounce"></div>
|
||||
<motion.div
|
||||
className="w-1 h-3 bg-neon rounded-full mt-2"
|
||||
animate={{ y: [0, 12, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Search, Shield, Cog, Zap } from 'lucide-react';
|
||||
import ScrollReveal from '../ScrollReveal';
|
||||
import { motion, useInView, useScroll, useTransform } from 'framer-motion';
|
||||
import { fadeInUp, scaleIn } from '@/utils/animations';
|
||||
|
||||
const ProcessTimeline = () => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef(null);
|
||||
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
|
||||
|
||||
// Smooth scroll-based timeline progress
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ["start end", "end start"]
|
||||
});
|
||||
const timelineProgress = useTransform(scrollYProgress, [0.2, 0.8], [0, 100]);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
@@ -63,7 +73,12 @@ const ProcessTimeline = () => {
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollReveal>
|
||||
<motion.div
|
||||
ref={headerRef}
|
||||
initial="hidden"
|
||||
animate={isHeaderInView ? "visible" : "hidden"}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||
How we <span className="text-neon">transform</span> your IT
|
||||
@@ -72,14 +87,14 @@ const ProcessTimeline = () => {
|
||||
Our proven four-phase methodology ensures systematic improvement and lasting results.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-8 lg:left-1/2 lg:transform lg:-translate-x-1/2 top-0 bottom-0 w-px bg-border">
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full bg-neon transition-all duration-500 ease-out"
|
||||
style={{ height: `${(activeStep + 1) * 25}%` }}
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 w-full bg-neon"
|
||||
style={{ height: useTransform(timelineProgress, (value) => `${value}%`) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -89,17 +104,35 @@ const ProcessTimeline = () => {
|
||||
const Icon = step.icon;
|
||||
const isActive = index <= activeStep;
|
||||
const isEven = index % 2 === 0;
|
||||
|
||||
const stepRef = useRef(null);
|
||||
const isInView = useInView(stepRef, { once: true, margin: "-150px" });
|
||||
|
||||
return (
|
||||
<ScrollReveal key={step.title} delay={index * 100}>
|
||||
<motion.div
|
||||
key={step.title}
|
||||
ref={stepRef}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
variants={fadeInUp}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<div className={`relative flex flex-col lg:flex-row items-center ${
|
||||
isEven ? '' : 'lg:flex-row-reverse'
|
||||
}`}>
|
||||
{/* Step content */}
|
||||
<div className={`flex-1 ${isEven ? 'lg:pr-16' : 'lg:pl-16'} ${
|
||||
isEven ? 'lg:text-right' : 'lg:text-left'
|
||||
} text-center lg:text-left`}>
|
||||
<div className="card-dark p-8 max-w-lg mx-auto lg:mx-0">
|
||||
<motion.div
|
||||
className={`flex-1 ${isEven ? 'lg:pr-16' : 'lg:pl-16'} ${
|
||||
isEven ? 'lg:text-right' : 'lg:text-left'
|
||||
} text-center lg:text-left`}
|
||||
initial={{ opacity: 0, x: isEven ? -40 : 40 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : { opacity: 0, x: isEven ? -40 : 40 }}
|
||||
transition={{ delay: index * 0.1 + 0.2, duration: 0.6 }}
|
||||
>
|
||||
<motion.div
|
||||
className="card-dark p-8 max-w-lg mx-auto lg:mx-0"
|
||||
whileHover={{ y: -5, boxShadow: "0 0 30px rgba(51, 102, 255, 0.3)" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<span className="text-sm font-medium text-neon uppercase tracking-wider">
|
||||
Step {index + 1}
|
||||
@@ -114,24 +147,34 @@ const ProcessTimeline = () => {
|
||||
<p className="text-sm text-foreground-muted">
|
||||
{step.details}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Timeline dot */}
|
||||
<div className="relative z-10 my-8 lg:my-0">
|
||||
<div className={`w-16 h-16 rounded-full border-4 flex items-center justify-center transition-all duration-500 ${
|
||||
isActive
|
||||
? 'border-neon bg-neon text-neon-foreground shadow-neon'
|
||||
: 'border-border bg-background text-foreground-muted'
|
||||
}`}>
|
||||
<motion.div
|
||||
className={`w-16 h-16 rounded-full border-4 flex items-center justify-center ${
|
||||
isActive
|
||||
? 'border-neon bg-neon text-neon-foreground shadow-neon'
|
||||
: 'border-border bg-background text-foreground-muted'
|
||||
}`}
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={isInView ? { scale: 1, rotate: 0 } : { scale: 0, rotate: -180 }}
|
||||
transition={{
|
||||
delay: index * 0.1 + 0.3,
|
||||
duration: 0.6,
|
||||
type: "spring",
|
||||
stiffness: 200
|
||||
}}
|
||||
>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Spacer for layout */}
|
||||
<div className="flex-1 hidden lg:block"></div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { MapPin, Star, Users } from 'lucide-react';
|
||||
import ScrollReveal from '../ScrollReveal';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import CountUpNumber from '../CountUpNumber';
|
||||
import { fadeInUp, scaleIn, staggerContainer, staggerItem } from '@/utils/animations';
|
||||
|
||||
const ProofSection = () => {
|
||||
const stats = [
|
||||
@@ -17,6 +19,13 @@ const ProofSection = () => {
|
||||
company: "Coastal Medical Group"
|
||||
};
|
||||
|
||||
const headerRef = useRef(null);
|
||||
const statsRef = useRef(null);
|
||||
const testimonialRef = useRef(null);
|
||||
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
|
||||
const isStatsInView = useInView(statsRef, { once: true, margin: "-100px" });
|
||||
const isTestimonialInView = useInView(testimonialRef, { once: true, margin: "-100px" });
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-background relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
@@ -24,12 +33,27 @@ const ProofSection = () => {
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-96 h-96 bg-neon/5 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollReveal>
|
||||
<motion.div
|
||||
ref={headerRef}
|
||||
initial="hidden"
|
||||
animate={isHeaderInView ? "visible" : "hidden"}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full bg-neon/10 border border-neon/30 text-neon text-sm font-medium mb-8">
|
||||
<motion.div
|
||||
className="inline-flex items-center px-4 py-2 rounded-full bg-neon/10 border border-neon/30 text-neon text-sm font-medium mb-8"
|
||||
animate={{
|
||||
boxShadow: [
|
||||
"0 0 20px rgba(51, 102, 255, 0.2)",
|
||||
"0 0 30px rgba(51, 102, 255, 0.4)",
|
||||
"0 0 20px rgba(51, 102, 255, 0.2)"
|
||||
]
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<MapPin className="w-4 h-4 mr-2" />
|
||||
Proudly serving the Coastal Bend
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||
Local expertise for{' '}
|
||||
@@ -42,48 +66,102 @@ const ProofSection = () => {
|
||||
tailored solutions that work in the real world.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<ScrollReveal delay={200}>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-20">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={stat.label} className="text-center">
|
||||
<div className="font-heading font-bold text-4xl lg:text-5xl text-neon mb-2">
|
||||
<CountUpNumber
|
||||
value={stat.value}
|
||||
duration={2000 + index * 200}
|
||||
className="inline-block"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-foreground-muted text-sm lg:text-base">
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
<motion.div
|
||||
ref={statsRef}
|
||||
initial="hidden"
|
||||
animate={isStatsInView ? "visible" : "hidden"}
|
||||
variants={staggerContainer}
|
||||
className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-20"
|
||||
>
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
variants={staggerItem}
|
||||
className="text-center"
|
||||
>
|
||||
<motion.div
|
||||
className="font-heading font-bold text-4xl lg:text-5xl text-neon mb-2"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={isStatsInView ? { scale: 1, rotate: 0 } : { scale: 0, rotate: -180 }}
|
||||
transition={{
|
||||
delay: index * 0.1,
|
||||
duration: 0.6,
|
||||
type: "spring",
|
||||
stiffness: 150
|
||||
}}
|
||||
>
|
||||
<CountUpNumber
|
||||
value={stat.value}
|
||||
duration={2000 + index * 200}
|
||||
className="inline-block"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="text-foreground-muted text-sm lg:text-base"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={isStatsInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 10 }}
|
||||
transition={{ delay: index * 0.1 + 0.3, duration: 0.5 }}
|
||||
>
|
||||
{stat.label}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Testimonial */}
|
||||
<ScrollReveal delay={400}>
|
||||
<div className="card-dark p-8 lg:p-12 max-w-4xl mx-auto">
|
||||
<motion.div
|
||||
ref={testimonialRef}
|
||||
initial="hidden"
|
||||
animate={isTestimonialInView ? "visible" : "hidden"}
|
||||
variants={scaleIn}
|
||||
>
|
||||
<motion.div
|
||||
className="card-dark p-8 lg:p-12 max-w-4xl mx-auto"
|
||||
whileHover={{ y: -5, boxShadow: "0 0 40px rgba(51, 102, 255, 0.3)" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row items-start gap-8">
|
||||
{/* Quote */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center mb-6">
|
||||
<motion.div
|
||||
className="flex items-center mb-6"
|
||||
initial="hidden"
|
||||
animate={isTestimonialInView ? "visible" : "hidden"}
|
||||
variants={staggerContainer}
|
||||
>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star key={i} className="w-5 h-5 text-neon fill-current" />
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={staggerItem}
|
||||
whileHover={{ scale: 1.2, rotate: 360 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<Star className="w-5 h-5 text-neon fill-current" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<blockquote className="text-lg lg:text-xl text-foreground leading-relaxed mb-6">
|
||||
"{testimonial.quote}"
|
||||
</blockquote>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 bg-neon/20 rounded-full flex items-center justify-center mr-4">
|
||||
<motion.div
|
||||
className="w-12 h-12 bg-neon/20 rounded-full flex items-center justify-center mr-4"
|
||||
animate={{
|
||||
boxShadow: [
|
||||
"0 0 10px rgba(51, 102, 255, 0.3)",
|
||||
"0 0 20px rgba(51, 102, 255, 0.5)",
|
||||
"0 0 10px rgba(51, 102, 255, 0.3)"
|
||||
]
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
<Users className="w-6 h-6 text-neon" />
|
||||
</div>
|
||||
</motion.div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">{testimonial.author}</div>
|
||||
<div className="text-foreground-muted text-sm">
|
||||
@@ -93,29 +171,43 @@ const ProofSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Service area */}
|
||||
<ScrollReveal delay={600}>
|
||||
<div className="mt-16 text-center">
|
||||
<h3 className="font-heading font-semibold text-xl text-foreground mb-6">
|
||||
Serving businesses throughout the Coastal Bend
|
||||
</h3>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isTestimonialInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ delay: 0.4, duration: 0.6 }}
|
||||
className="mt-16 text-center"
|
||||
>
|
||||
<h3 className="font-heading font-semibold text-xl text-foreground mb-6">
|
||||
Serving businesses throughout the Coastal Bend
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap justify-center items-center gap-6 text-foreground-muted">
|
||||
{[
|
||||
'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass',
|
||||
'Rockport', 'Fulton', 'Sinton', 'Mathis'
|
||||
].map((city) => (
|
||||
<span key={city} className="flex items-center text-sm">
|
||||
<MapPin className="w-3 h-3 mr-1 text-neon" />
|
||||
{city}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
<motion.div
|
||||
className="flex flex-wrap justify-center items-center gap-6 text-foreground-muted"
|
||||
initial="hidden"
|
||||
animate={isTestimonialInView ? "visible" : "hidden"}
|
||||
variants={staggerContainer}
|
||||
>
|
||||
{[
|
||||
'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass',
|
||||
'Rockport', 'Fulton', 'Sinton', 'Mathis'
|
||||
].map((city) => (
|
||||
<motion.span
|
||||
key={city}
|
||||
className="flex items-center text-sm"
|
||||
variants={staggerItem}
|
||||
whileHover={{ scale: 1.1, color: "rgba(51, 102, 255, 1)" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MapPin className="w-3 h-3 mr-1 text-neon" />
|
||||
{city}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Monitor, Wifi, Cloud, Shield, Database, Settings } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ScrollReveal from '../ScrollReveal';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { fadeInUp, staggerContainer, staggerItem, cardHover, glowHover } from '@/utils/animations';
|
||||
|
||||
const ServicesOverview = () => {
|
||||
const services = [
|
||||
@@ -42,6 +44,11 @@ const ServicesOverview = () => {
|
||||
}
|
||||
];
|
||||
|
||||
const headerRef = useRef(null);
|
||||
const gridRef = useRef(null);
|
||||
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
|
||||
const isGridInView = useInView(gridRef, { once: true, margin: "-100px" });
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-background-deep relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
@@ -50,7 +57,12 @@ const ServicesOverview = () => {
|
||||
<div className="absolute bottom-1/4 left-0 w-96 h-96 bg-neon/5 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollReveal>
|
||||
<motion.div
|
||||
ref={headerRef}
|
||||
initial="hidden"
|
||||
animate={isHeaderInView ? "visible" : "hidden"}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||
Complete IT solutions for{' '}
|
||||
@@ -60,25 +72,42 @@ const ServicesOverview = () => {
|
||||
From desktop support to enterprise infrastructure — we've got your technology covered.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<motion.div
|
||||
ref={gridRef}
|
||||
initial="hidden"
|
||||
animate={isGridInView ? "visible" : "hidden"}
|
||||
variants={staggerContainer}
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
{services.map((service, index) => {
|
||||
const Icon = service.icon;
|
||||
|
||||
|
||||
return (
|
||||
<ScrollReveal key={service.title} delay={index * 100}>
|
||||
<div className="card-dark p-8 group hover:shadow-neon transition-all duration-500 hover:-translate-y-1">
|
||||
<motion.div key={service.title} variants={staggerItem}>
|
||||
<motion.div
|
||||
className="card-dark p-8 h-full"
|
||||
whileHover={{
|
||||
y: -8,
|
||||
boxShadow: "0 0 30px rgba(51, 102, 255, 0.4)"
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center mb-6 group-hover:bg-neon/30 transition-colors">
|
||||
<motion.div
|
||||
className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center mb-6"
|
||||
whileHover={{ rotate: 360, scale: 1.1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<Icon className="w-6 h-6 text-neon" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="font-heading font-bold text-xl text-foreground mb-4">
|
||||
{service.title}
|
||||
</h3>
|
||||
|
||||
|
||||
<p className="text-foreground-muted mb-6 leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
@@ -87,7 +116,18 @@ const ServicesOverview = () => {
|
||||
<ul className="space-y-2 mb-6">
|
||||
{service.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center text-sm text-foreground-muted">
|
||||
<div className="w-1.5 h-1.5 bg-neon rounded-full mr-3"></div>
|
||||
<motion.div
|
||||
className="w-1.5 h-1.5 bg-neon rounded-full mr-3"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.7, 1, 0.7]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
delay: index * 0.2
|
||||
}}
|
||||
/>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
@@ -96,30 +136,40 @@ const ServicesOverview = () => {
|
||||
{/* CTA */}
|
||||
<Link
|
||||
to="/services"
|
||||
className="inline-flex items-center text-neon font-medium hover:text-neon/80 transition-colors group-hover:underline"
|
||||
className="inline-flex items-center text-neon font-medium hover:text-neon/80 transition-colors group"
|
||||
>
|
||||
Learn more
|
||||
<svg className="w-4 h-4 ml-1 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<motion.svg
|
||||
className="w-4 h-4 ml-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
animate={{ x: [0, 3, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</motion.svg>
|
||||
</Link>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<ScrollReveal delay={600}>
|
||||
<div className="text-center mt-16">
|
||||
<Link
|
||||
to="/services"
|
||||
className="btn-ghost text-lg px-12 py-4"
|
||||
>
|
||||
View all services
|
||||
</Link>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isGridInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ delay: 0.8, duration: 0.6 }}
|
||||
className="text-center mt-16"
|
||||
>
|
||||
<Link
|
||||
to="/services"
|
||||
className="btn-ghost text-lg px-12 py-4"
|
||||
>
|
||||
View all services
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,58 @@
|
||||
import { Shield, Zap, Users, ArrowRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ScrollReveal from '../ScrollReveal';
|
||||
import { motion, useInView, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||
import { useRef, MouseEvent } from 'react';
|
||||
import { fadeInUp, slideInLeft, slideInRight, buttonHover, buttonTap, cardHover } from '@/utils/animations';
|
||||
|
||||
// Optimized 3D Tilt Card Component with reduced effect
|
||||
const TiltCard = ({ children, className = '' }: { children: React.ReactNode; className?: string }) => {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
|
||||
// Reduced rotation range for subtler effect (10 -> 5 degrees)
|
||||
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [5, -5]), {
|
||||
stiffness: 200,
|
||||
damping: 25,
|
||||
});
|
||||
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-5, 5]), {
|
||||
stiffness: 200,
|
||||
damping: 25,
|
||||
});
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
mouseX.set((e.clientX - centerX) / (rect.width / 2));
|
||||
mouseY.set((e.clientY - centerY) / (rect.height / 2));
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouseX.set(0);
|
||||
mouseY.set(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={cardRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{
|
||||
rotateX,
|
||||
rotateY,
|
||||
transformStyle: 'preserve-3d',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
className={className}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const ValuePillars = () => {
|
||||
const pillars = [
|
||||
@@ -27,13 +79,21 @@ const ValuePillars = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const headerRef = useRef(null);
|
||||
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-background-deep relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 grid-overlay opacity-20"></div>
|
||||
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollReveal>
|
||||
<motion.div
|
||||
ref={headerRef}
|
||||
initial="hidden"
|
||||
animate={isHeaderInView ? "visible" : "hidden"}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||
Why teams choose us for{' '}
|
||||
@@ -43,58 +103,96 @@ const ValuePillars = () => {
|
||||
We handle the complexity so you can focus on what you do best.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-24">
|
||||
{pillars.map((pillar, index) => {
|
||||
const Icon = pillar.icon;
|
||||
const isReverse = index % 2 === 1;
|
||||
|
||||
const itemRef = useRef(null);
|
||||
const isInView = useInView(itemRef, { once: true, margin: "-100px" });
|
||||
|
||||
return (
|
||||
<ScrollReveal key={pillar.number} delay={index * 200}>
|
||||
<motion.div
|
||||
key={pillar.number}
|
||||
ref={itemRef}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
variants={isReverse ? slideInRight : slideInLeft}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<div className={`flex flex-col ${isReverse ? 'lg:flex-row-reverse' : 'lg:flex-row'} items-center gap-12 lg:gap-16`}>
|
||||
{/* Content */}
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-6xl font-heading font-bold text-neon/30">
|
||||
<motion.span
|
||||
className="text-6xl font-heading font-bold text-neon/30"
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={isInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.5 }}
|
||||
transition={{ delay: 0.4, duration: 0.6, type: "spring" }}
|
||||
>
|
||||
{pillar.number}
|
||||
</span>
|
||||
<div className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center">
|
||||
</motion.span>
|
||||
<motion.div
|
||||
className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center"
|
||||
initial={{ opacity: 0, rotate: -180 }}
|
||||
animate={isInView ? { opacity: 1, rotate: 0 } : { opacity: 0, rotate: -180 }}
|
||||
transition={{ delay: 0.5, duration: 0.6 }}
|
||||
>
|
||||
<Icon className="w-6 h-6 text-neon" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 className="font-heading font-bold text-3xl text-foreground">
|
||||
{pillar.title}
|
||||
</h3>
|
||||
|
||||
|
||||
<p className="text-lg text-foreground-muted leading-relaxed">
|
||||
{pillar.description}
|
||||
</p>
|
||||
|
||||
<Link
|
||||
to="/services"
|
||||
className="btn-ghost group flex items-center space-x-2 w-fit"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
<span>Learn more</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
||||
</Link>
|
||||
|
||||
<motion.div whileHover={buttonHover} whileTap={buttonTap}>
|
||||
<Link
|
||||
to="/services"
|
||||
className="btn-ghost group flex items-center space-x-2 w-fit"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
<span>Learn more</span>
|
||||
<motion.div
|
||||
animate={{ x: [0, 3, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</motion.div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="flex-1 parallax">
|
||||
{/* Image with 3D Tilt Effect */}
|
||||
<TiltCard className="flex-1">
|
||||
<div className="card-dark p-2 group hover:shadow-neon transition-all duration-500">
|
||||
<img
|
||||
src={pillar.image}
|
||||
alt={pillar.title}
|
||||
className="w-full h-64 lg:h-80 object-cover rounded-xl transition-transform duration-500 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="relative overflow-hidden rounded-xl">
|
||||
<motion.img
|
||||
src={pillar.image}
|
||||
alt={pillar.title}
|
||||
className="w-full h-64 lg:h-80 object-cover"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
loading="lazy"
|
||||
style={{ willChange: 'transform' }}
|
||||
/>
|
||||
{/* Simplified shine effect on hover */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/8 to-transparent pointer-events-none"
|
||||
initial={{ x: '-100%', y: '-100%' }}
|
||||
whileHover={{ x: '100%', y: '100%' }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TiltCard>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
170
src/index.css
170
src/index.css
@@ -88,11 +88,36 @@
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Accessibility: Focus visible styles */
|
||||
*:focus-visible {
|
||||
outline: 2px solid hsl(var(--neon));
|
||||
outline-offset: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Skip link for keyboard navigation */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: hsl(var(--neon));
|
||||
color: hsl(var(--background));
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
z-index: 100;
|
||||
font-weight: 600;
|
||||
border-radius: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@@ -101,6 +126,29 @@
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Screen reader only class */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.sr-only:focus,
|
||||
.sr-only:active {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@@ -148,6 +196,26 @@
|
||||
@apply rounded-[var(--radius)] px-8 py-4 font-semibold;
|
||||
@apply transition-all duration-300 ease-out;
|
||||
@apply shadow-[0_0_0_1px_hsl(var(--neon))] hover:shadow-[0_0_20px_hsl(var(--neon)/0.5)];
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-neon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.btn-neon:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@@ -155,16 +223,116 @@
|
||||
@apply rounded-[var(--radius)] px-8 py-4 font-semibold;
|
||||
@apply transition-all duration-300 ease-out;
|
||||
@apply hover:shadow-[0_0_15px_hsl(var(--neon)/0.3)];
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-ghost::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right, transparent, hsl(var(--neon)), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.5s ease-out;
|
||||
}
|
||||
|
||||
.btn-ghost:hover::after {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card-dark {
|
||||
@apply bg-card border border-card-border rounded-[var(--radius-lg)];
|
||||
@apply backdrop-blur-sm shadow-[var(--shadow-card)];
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-dark::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg,
|
||||
transparent 30%,
|
||||
hsl(var(--neon) / 0.1) 50%,
|
||||
transparent 70%);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.card-dark:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-dark:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: hsl(var(--neon) / 0.3);
|
||||
}
|
||||
|
||||
/* Typography helpers */
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* Smooth focus ring for better UX */
|
||||
.focus-ring {
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-neon focus:ring-offset-2 focus:ring-offset-background;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Magnetic hover effect for interactive elements */
|
||||
.magnetic-hover {
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.magnetic-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Glassmorphism effect */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Shimmer effect for loading states */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shimmer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(51, 102, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,47 @@ const Contact = () => {
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Service Area Map */}
|
||||
<section className="py-16 bg-background-deep">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ScrollReveal>
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="font-heading font-bold text-3xl text-foreground mb-4">
|
||||
Our Service Area
|
||||
</h2>
|
||||
<p className="text-foreground-muted max-w-2xl mx-auto">
|
||||
Proudly serving Corpus Christi, Portland, Rockport, Aransas Pass, Kingsville, Port Aransas, and the entire Coastal Bend region.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="card-dark p-2 overflow-hidden">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d113726.84791849895!2d-97.48659164550781!3d27.800587899999997!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x8668fa3c40818e93%3A0x4e3c0a1c2bef9c65!2sCorpus%20Christi%2C%20TX!5e0!3m2!1sen!2sus!4v1736364000000!5m2!1sen!2sus"
|
||||
width="100%"
|
||||
height="450"
|
||||
style={{ border: 0, borderRadius: '1rem' }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Bay Area Affiliates Service Area - Corpus Christi and Coastal Bend"
|
||||
aria-label="Google Maps showing our service area in Corpus Christi and the Coastal Bend region"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville', 'Port Aransas'].map((city) => (
|
||||
<div key={city} className="text-center">
|
||||
<div className="card-dark p-3">
|
||||
<MapPin className="w-5 h-5 text-neon mx-auto mb-1" />
|
||||
<p className="text-xs text-foreground font-medium">{city}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
@@ -11,7 +11,7 @@ const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navigation />
|
||||
<main>
|
||||
<main id="main-content" role="main">
|
||||
<HeroSection />
|
||||
<ValuePillars />
|
||||
<ProcessTimeline />
|
||||
|
||||
210
src/utils/animations.ts
Normal file
210
src/utils/animations.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Variants } from 'framer-motion';
|
||||
|
||||
// Smooth easing curves
|
||||
export const easing = {
|
||||
smooth: [0.6, 0.01, 0.05, 0.95],
|
||||
snappy: [0.25, 0.46, 0.45, 0.94],
|
||||
bouncy: [0.68, -0.55, 0.265, 1.55],
|
||||
elegant: [0.43, 0.13, 0.23, 0.96],
|
||||
};
|
||||
|
||||
// Hero section animations
|
||||
export const heroVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.15,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const heroItemVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Fade in up animation
|
||||
export const fadeInUp: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 60,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.7,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Scale in animation
|
||||
export const scaleIn: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: easing.smooth,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Slide in from left
|
||||
export const slideInLeft: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
x: -60,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.7,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Slide in from right
|
||||
export const slideInRight: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
x: 60,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.7,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Stagger container
|
||||
export const staggerContainer: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.12,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Stagger item
|
||||
export const staggerItem: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Button hover animation
|
||||
export const buttonHover = {
|
||||
scale: 1.02,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: easing.snappy,
|
||||
},
|
||||
};
|
||||
|
||||
export const buttonTap = {
|
||||
scale: 0.98,
|
||||
};
|
||||
|
||||
// Card hover animation
|
||||
export const cardHover = {
|
||||
y: -8,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: easing.smooth,
|
||||
},
|
||||
};
|
||||
|
||||
// Glow effect
|
||||
export const glowHover = {
|
||||
boxShadow: '0 0 30px rgba(51, 102, 255, 0.6)',
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
};
|
||||
|
||||
// Navigation animation
|
||||
export const navVariants: Variants = {
|
||||
hidden: {
|
||||
y: -100,
|
||||
opacity: 0,
|
||||
},
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Page transition
|
||||
export const pageTransition = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -20 },
|
||||
transition: { duration: 0.4, ease: easing.elegant },
|
||||
};
|
||||
|
||||
// Parallax scroll effect
|
||||
export const parallaxScroll = (scrollY: number, factor: number = 0.5) => ({
|
||||
y: scrollY * factor,
|
||||
transition: { type: 'tween', ease: 'linear', duration: 0 },
|
||||
});
|
||||
|
||||
// Scroll reveal with intersection observer
|
||||
export const scrollRevealVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.7,
|
||||
ease: easing.elegant,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Magnetic button effect (advanced)
|
||||
export const magneticEffect = (x: number, y: number, strength: number = 0.3) => ({
|
||||
x: x * strength,
|
||||
y: y * strength,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 150,
|
||||
damping: 15,
|
||||
mass: 0.1,
|
||||
},
|
||||
});
|
||||
100
src/utils/reportWebVitals.ts
Normal file
100
src/utils/reportWebVitals.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { onCLS, onFCP, onLCP, onTTFB, onINP, type Metric } from 'web-vitals';
|
||||
|
||||
function sendToAnalytics(metric: Metric) {
|
||||
// Log to console in development
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Web Vitals:', {
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
delta: metric.delta,
|
||||
id: metric.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Send to Google Analytics if available
|
||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||
(window as any).gtag('event', metric.name, {
|
||||
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
|
||||
event_category: 'Web Vitals',
|
||||
event_label: metric.id,
|
||||
non_interaction: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Send to custom analytics endpoint if needed
|
||||
if (import.meta.env.PROD && import.meta.env.VITE_ANALYTICS_ENDPOINT) {
|
||||
fetch(import.meta.env.VITE_ANALYTICS_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
metric: metric.name,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
id: metric.id,
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
}),
|
||||
keepalive: true,
|
||||
}).catch((error) => {
|
||||
console.error('Failed to send web vitals:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function reportWebVitals() {
|
||||
// Core Web Vitals
|
||||
onCLS(sendToAnalytics);
|
||||
onLCP(sendToAnalytics);
|
||||
onINP(sendToAnalytics); // Replaces deprecated FID
|
||||
|
||||
// Other important metrics
|
||||
onFCP(sendToAnalytics);
|
||||
onTTFB(sendToAnalytics);
|
||||
}
|
||||
|
||||
// Performance observer for additional metrics
|
||||
export function observePerformance() {
|
||||
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Observe long tasks (blocking the main thread)
|
||||
try {
|
||||
const longTaskObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration > 50) {
|
||||
console.warn('Long Task detected:', {
|
||||
duration: entry.duration,
|
||||
startTime: entry.startTime,
|
||||
name: entry.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
longTaskObserver.observe({ entryTypes: ['longtask'] });
|
||||
} catch (e) {
|
||||
// Long task API not supported
|
||||
}
|
||||
|
||||
// Observe layout shifts
|
||||
try {
|
||||
const layoutShiftObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if ((entry as any).hadRecentInput) continue;
|
||||
const value = (entry as any).value;
|
||||
if (value > 0.1) {
|
||||
console.warn('Large Layout Shift:', {
|
||||
value,
|
||||
startTime: entry.startTime,
|
||||
sources: (entry as any).sources,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
layoutShiftObserver.observe({ entryTypes: ['layout-shift'] });
|
||||
} catch (e) {
|
||||
// Layout shift API not supported
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user