Compare commits
No commits in common. "lighthouse" and "main" have entirely different histories.
lighthouse
...
main
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(npm run build:*)",
|
|
||||||
"Bash(npm install:*)",
|
|
||||||
"Bash(ls:*)",
|
|
||||||
"Bash(node scripts/optimize-images.js:*)",
|
|
||||||
"Bash(node scripts/optimize-images.cjs:*)",
|
|
||||||
"Skill(frontend-design)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
index.html
|
|
@ -3,9 +3,9 @@
|
||||||
<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>IT Support Corpus Christi ⚡ 24/7 Managed Services | Bay Area Affiliates</title>
|
<title>Managed IT Services Corpus Christi | Bay Area Affiliates</title>
|
||||||
<meta name="description" content="Stop IT headaches! 25+ years experience, 2-hour response time, 24/7 monitoring. Managed IT services in Corpus Christi & Coastal Bend. Free assessment. Call (361) 765-8400" />
|
<meta name="description" content="Secure, tailored IT support—Corpus Christi's trusted experts for 25+ years. Call today for a free assessment." />
|
||||||
<meta name="keywords" content="managed IT services corpus christi, IT support coastal bend, 24/7 IT monitoring, business computer solutions, Windows 11 migration, VPN setup, network security corpus christi, same-day IT support" />
|
<meta name="keywords" content="managed IT services corpus christi, IT support coastal bend, business computer solutions, Portland IT services, computer repair corpus christi" />
|
||||||
<meta name="author" content="Bay Area Affiliates, Inc." />
|
<meta name="author" content="Bay Area Affiliates, Inc." />
|
||||||
|
|
||||||
<meta property="og:title" content="Corpus Christi Managed IT Experts. Reliable, Secure, Local." />
|
<meta property="og:title" content="Corpus Christi Managed IT Experts. Reliable, Secure, Local." />
|
||||||
|
|
@ -211,33 +211,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Performance Optimization -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
|
|
||||||
|
|
||||||
<!-- Non-blocking font loading with font-display: swap -->
|
|
||||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700;800&display=swap" />
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700;800&display=swap" media="print" onload="this.media='all'" />
|
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700;800&display=swap" /></noscript>
|
|
||||||
|
|
||||||
<!-- Critical asset preloads -->
|
|
||||||
<link rel="preload" href="/logo_bayarea.svg" as="image" />
|
|
||||||
<link rel="preload" href="/serverroom.webp" as="image" type="image/webp" />
|
|
||||||
|
|
||||||
<!-- Canonical URL -->
|
|
||||||
<link rel="canonical" href="https://bayarea-cc.com/" />
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo_bayarea.svg" />
|
<link rel="icon" type="image/svg+xml" href="/logo_bayarea.svg" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/logo_bayarea.svg" />
|
<link rel="icon" type="image/png" href="/logo_bayarea.svg" />
|
||||||
|
<!-- Note: For production, consider converting logo_bayarea.svg to favicon.ico for better browser support -->
|
||||||
<!-- PWA / Mobile -->
|
|
||||||
<meta name="theme-color" content="#3366ff" />
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.24.12",
|
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
|
@ -74,13 +73,10 @@
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"lovable-tagger": "^1.1.9",
|
"lovable-tagger": "^1.1.9",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"sharp": "^0.34.5",
|
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"terser": "^5.44.1",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.38.0",
|
"typescript-eslint": "^8.38.0",
|
||||||
"vite": "^5.4.19",
|
"vite": "^5.4.19"
|
||||||
"web-vitals": "^5.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|
@ -154,17 +150,6 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
|
||||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
||||||
|
|
@ -874,496 +859,6 @@
|
||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/colour": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-darwin-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-arm": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-ppc64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-riscv64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-s390x": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-wasm32": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/runtime": "^1.7.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-win32-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-win32-ia32": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-win32-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
|
@ -1413,17 +908,6 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/source-map": {
|
|
||||||
"version": "0.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
|
||||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.25"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
|
@ -4027,13 +3511,6 @@
|
||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-from": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
|
@ -4391,16 +3868,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-node-es": {
|
"node_modules/detect-node-es": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
|
@ -4895,33 +4362,6 @@
|
||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
|
||||||
"version": "12.24.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.24.12.tgz",
|
|
||||||
"integrity": "sha512-W+tBOI1SDGNMH4D4mADY95qYd16Drke2Tj9zlGlwTGSCi6yy8wbMmPY1mvirfcTK8HBeuuCd2PflHdN/zbL4ew==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"motion-dom": "^12.24.11",
|
|
||||||
"motion-utils": "^12.24.10",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emotion/is-prop-valid": "*",
|
|
||||||
"react": "^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@emotion/is-prop-valid": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -5434,21 +4874,6 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-dom": {
|
|
||||||
"version": "12.24.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.24.11.tgz",
|
|
||||||
"integrity": "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"motion-utils": "^12.24.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/motion-utils": {
|
|
||||||
"version": "12.24.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz",
|
|
||||||
"integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
@ -6258,9 +5683,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -6270,51 +5695,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@img/colour": "^1.0.0",
|
|
||||||
"detect-libc": "^2.1.2",
|
|
||||||
"semver": "^7.7.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-darwin-arm64": "0.34.5",
|
|
||||||
"@img/sharp-darwin-x64": "0.34.5",
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
|
||||||
"@img/sharp-linux-arm": "0.34.5",
|
|
||||||
"@img/sharp-linux-arm64": "0.34.5",
|
|
||||||
"@img/sharp-linux-ppc64": "0.34.5",
|
|
||||||
"@img/sharp-linux-riscv64": "0.34.5",
|
|
||||||
"@img/sharp-linux-s390x": "0.34.5",
|
|
||||||
"@img/sharp-linux-x64": "0.34.5",
|
|
||||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
|
||||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
|
||||||
"@img/sharp-wasm32": "0.34.5",
|
|
||||||
"@img/sharp-win32-arm64": "0.34.5",
|
|
||||||
"@img/sharp-win32-ia32": "0.34.5",
|
|
||||||
"@img/sharp-win32-x64": "0.34.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|
@ -6358,16 +5738,6 @@
|
||||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|
@ -6377,17 +5747,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-support": {
|
|
||||||
"version": "0.5.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
|
||||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"buffer-from": "^1.0.0",
|
|
||||||
"source-map": "^0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
|
@ -6600,32 +5959,6 @@
|
||||||
"tailwindcss": ">=3.0.0 || insiders"
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
|
||||||
"version": "5.44.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
|
||||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
|
||||||
"acorn": "^8.15.0",
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"terser": "bin/terser"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/terser/node_modules/commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
|
|
@ -7372,13 +6705,6 @@
|
||||||
"@esbuild/win32-x64": "0.21.5"
|
"@esbuild/win32-x64": "0.21.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/web-vitals": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.24.12",
|
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
|
@ -77,12 +76,9 @@
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"lovable-tagger": "^1.1.9",
|
"lovable-tagger": "^1.1.9",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"sharp": "^0.34.5",
|
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"terser": "^5.44.1",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.38.0",
|
"typescript-eslint": "^8.38.0",
|
||||||
"vite": "^5.4.19",
|
"vite": "^5.4.19"
|
||||||
"web-vitals": "^5.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
# Security Headers for all routes
|
|
||||||
/*
|
|
||||||
# XSS Protection
|
|
||||||
X-Frame-Options: DENY
|
|
||||||
X-Content-Type-Options: nosniff
|
|
||||||
X-XSS-Protection: 1; mode=block
|
|
||||||
|
|
||||||
# Referrer Policy
|
|
||||||
Referrer-Policy: strict-origin-when-cross-origin
|
|
||||||
|
|
||||||
# Permissions Policy
|
|
||||||
Permissions-Policy: geolocation=(self), microphone=(), camera=(), payment=()
|
|
||||||
|
|
||||||
# Strict Transport Security (HSTS)
|
|
||||||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
|
||||||
|
|
||||||
# Content Security Policy
|
|
||||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https: blob:; connect-src 'self' https://www.google-analytics.com https://*.googletagmanager.com; frame-ancestors 'none';
|
|
||||||
|
|
||||||
# Cache Control for static assets
|
|
||||||
Cache-Control: public, max-age=31536000, immutable
|
|
||||||
|
|
||||||
# Cache Control for HTML
|
|
||||||
/*.html
|
|
||||||
Cache-Control: public, max-age=0, must-revalidate
|
|
||||||
|
|
||||||
# Cache Control for service worker
|
|
||||||
/sw.js
|
|
||||||
Cache-Control: public, max-age=0, must-revalidate
|
|
||||||
|
|
||||||
# Cache Control for manifest
|
|
||||||
/manifest.json
|
|
||||||
Cache-Control: public, max-age=86400, must-revalidate
|
|
||||||
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 75 KiB |
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Bay Area Affiliates - Managed IT Services",
|
|
||||||
"short_name": "Bay Area IT",
|
|
||||||
"description": "Professional managed IT services in Corpus Christi & Coastal Bend",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#030303",
|
|
||||||
"theme_color": "#3366ff",
|
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/logo_bayarea.svg",
|
|
||||||
"sizes": "any",
|
|
||||||
"type": "image/svg+xml",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": ["business", "productivity", "utilities"],
|
|
||||||
"scope": "/",
|
|
||||||
"lang": "en-US",
|
|
||||||
"dir": "ltr"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 56 KiB |
|
|
@ -1,113 +0,0 @@
|
||||||
const sharp = require('sharp');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const PUBLIC_DIR = path.join(__dirname, '../public');
|
|
||||||
const WEBP_QUALITY = 80;
|
|
||||||
const AVIF_QUALITY = 80;
|
|
||||||
const PNG_QUALITY = 80;
|
|
||||||
|
|
||||||
async function optimizeImages() {
|
|
||||||
console.log('Starting image optimization...\n');
|
|
||||||
|
|
||||||
// Get all PNG files in public directory
|
|
||||||
const files = fs.readdirSync(PUBLIC_DIR)
|
|
||||||
.filter(file => file.endsWith('.png'))
|
|
||||||
.map(file => path.join(PUBLIC_DIR, file));
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
console.log('No PNG files found in public directory');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${files.length} PNG files to optimize\n`);
|
|
||||||
|
|
||||||
let totalOriginalSize = 0;
|
|
||||||
let totalWebpSize = 0;
|
|
||||||
let totalAvifSize = 0;
|
|
||||||
let totalCompressedPngSize = 0;
|
|
||||||
|
|
||||||
for (const filePath of files) {
|
|
||||||
const filename = path.basename(filePath);
|
|
||||||
const filenameWithoutExt = path.basename(filePath, '.png');
|
|
||||||
const originalSize = fs.statSync(filePath).size;
|
|
||||||
totalOriginalSize += originalSize;
|
|
||||||
|
|
||||||
console.log(`Processing: ${filename} (${(originalSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Generate WebP version
|
|
||||||
const webpPath = path.join(PUBLIC_DIR, `${filenameWithoutExt}.webp`);
|
|
||||||
await sharp(filePath)
|
|
||||||
.resize(1920, null, { withoutEnlargement: true, fit: 'inside' })
|
|
||||||
.webp({ quality: WEBP_QUALITY })
|
|
||||||
.toFile(webpPath);
|
|
||||||
|
|
||||||
const webpSize = fs.statSync(webpPath).size;
|
|
||||||
totalWebpSize += webpSize;
|
|
||||||
console.log(` ✓ WebP: ${(webpSize / 1024).toFixed(2)} KB (${((1 - webpSize / originalSize) * 100).toFixed(1)}% reduction)`);
|
|
||||||
|
|
||||||
// Generate AVIF version
|
|
||||||
const avifPath = path.join(PUBLIC_DIR, `${filenameWithoutExt}.avif`);
|
|
||||||
await sharp(filePath)
|
|
||||||
.resize(1920, null, { withoutEnlargement: true, fit: 'inside' })
|
|
||||||
.avif({ quality: AVIF_QUALITY })
|
|
||||||
.toFile(avifPath);
|
|
||||||
|
|
||||||
const avifSize = fs.statSync(avifPath).size;
|
|
||||||
totalAvifSize += avifSize;
|
|
||||||
console.log(` ✓ AVIF: ${(avifSize / 1024).toFixed(2)} KB (${((1 - avifSize / originalSize) * 100).toFixed(1)}% reduction)`);
|
|
||||||
|
|
||||||
// Compress original PNG
|
|
||||||
const compressedPngPath = path.join(PUBLIC_DIR, `${filenameWithoutExt}_compressed.png`);
|
|
||||||
await sharp(filePath)
|
|
||||||
.resize(1920, null, { withoutEnlargement: true, fit: 'inside' })
|
|
||||||
.png({
|
|
||||||
quality: PNG_QUALITY,
|
|
||||||
compressionLevel: 9,
|
|
||||||
palette: true
|
|
||||||
})
|
|
||||||
.toFile(compressedPngPath);
|
|
||||||
|
|
||||||
const compressedPngSize = fs.statSync(compressedPngPath).size;
|
|
||||||
totalCompressedPngSize += compressedPngSize;
|
|
||||||
console.log(` ✓ Compressed PNG: ${(compressedPngSize / 1024).toFixed(2)} KB (${((1 - compressedPngSize / originalSize) * 100).toFixed(1)}% reduction)`);
|
|
||||||
|
|
||||||
// Replace original with compressed version
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
fs.renameSync(compressedPngPath, filePath);
|
|
||||||
console.log(` ✓ Original PNG replaced with compressed version\n`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(` ✗ Error processing ${filename}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
console.log('\n' + '='.repeat(60));
|
|
||||||
console.log('OPTIMIZATION SUMMARY');
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
console.log(`Total original PNG size: ${(totalOriginalSize / 1024 / 1024).toFixed(2)} MB`);
|
|
||||||
console.log(`Total WebP size: ${(totalWebpSize / 1024 / 1024).toFixed(2)} MB (${((1 - totalWebpSize / totalOriginalSize) * 100).toFixed(1)}% reduction)`);
|
|
||||||
console.log(`Total AVIF size: ${(totalAvifSize / 1024 / 1024).toFixed(2)} MB (${((1 - totalAvifSize / totalOriginalSize) * 100).toFixed(1)}% reduction)`);
|
|
||||||
console.log(`Total compressed PNG size: ${(totalCompressedPngSize / 1024 / 1024).toFixed(2)} MB (${((1 - totalCompressedPngSize / totalOriginalSize) * 100).toFixed(1)}% reduction)`);
|
|
||||||
console.log('\nAverage file sizes:');
|
|
||||||
console.log(` WebP: ${(totalWebpSize / files.length / 1024).toFixed(2)} KB`);
|
|
||||||
console.log(` AVIF: ${(totalAvifSize / files.length / 1024).toFixed(2)} KB`);
|
|
||||||
console.log(` Compressed PNG: ${(totalCompressedPngSize / files.length / 1024).toFixed(2)} KB`);
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
|
|
||||||
// Check if targets are met
|
|
||||||
const avgWebpSize = totalWebpSize / files.length / 1024;
|
|
||||||
const avgAvifSize = totalAvifSize / files.length / 1024;
|
|
||||||
const totalSizeAfter = (totalWebpSize + totalAvifSize + totalCompressedPngSize) / 1024 / 1024;
|
|
||||||
|
|
||||||
console.log('\nTarget Achievement:');
|
|
||||||
console.log(` WebP avg < 200KB: ${avgWebpSize < 200 ? '✓ PASS' : '✗ FAIL'} (${avgWebpSize.toFixed(2)} KB)`);
|
|
||||||
console.log(` AVIF avg < 150KB: ${avgAvifSize < 150 ? '✓ PASS' : '✗ FAIL'} (${avgAvifSize.toFixed(2)} KB)`);
|
|
||||||
console.log(` Total size < 2MB: ${totalSizeAfter < 2 ? '✓ PASS' : '✗ FAIL'} (${totalSizeAfter.toFixed(2)} MB)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizeImages()
|
|
||||||
.then(() => console.log('\nOptimization complete!'))
|
|
||||||
.catch(error => console.error('Optimization failed:', error));
|
|
||||||
117
src/App.tsx
|
|
@ -2,12 +2,8 @@ import { Toaster } from "@/components/ui/toaster";
|
||||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import { lazy, Suspense, useEffect } from "react";
|
import { lazy, Suspense } 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
|
// Eager load critical pages for better initial performance
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
|
|
@ -30,101 +26,42 @@ const NetworkAttachedStorage = lazy(() => import("./pages/NetworkAttachedStorage
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
// Loading fallback component with animation
|
// Loading fallback component
|
||||||
const PageLoader = () => (
|
const PageLoader = () => (
|
||||||
<motion.div
|
<div className="min-h-screen flex items-center justify-center bg-background-deep">
|
||||||
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="text-center">
|
||||||
<motion.div
|
<div className="w-16 h-16 border-4 border-neon/30 border-t-neon rounded-full animate-spin mx-auto mb-4"></div>
|
||||||
className="w-16 h-16 border-4 border-neon/30 border-t-neon rounded-full mx-auto mb-4"
|
<p className="text-foreground-muted">Loading...</p>
|
||||||
animate={{ rotate: 360 }}
|
</div>
|
||||||
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 = () => (
|
const App = () => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Sonner />
|
<Sonner />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppContent />
|
<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>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -20,7 +20,7 @@ const Footer = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer role="contentinfo" className="bg-background-deep border-t border-border">
|
<footer className="bg-background-deep border-t border-border">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
|
||||||
{/* Company info */}
|
{/* Company info */}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import { navVariants, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations';
|
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -29,146 +27,65 @@ const Navigation = () => {
|
||||||
const isActive = (path: string) => location.pathname === path;
|
const isActive = (path: string) => location.pathname === path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
||||||
{/* Skip link for accessibility */}
|
? 'bg-white/5 backdrop-blur-2xl border-b border-white/20 shadow-2xl shadow-black/20'
|
||||||
<a href="#main-content" className="skip-link">
|
: 'bg-transparent'
|
||||||
Skip to main content
|
}`}>
|
||||||
</a>
|
|
||||||
|
|
||||||
<motion.nav
|
|
||||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
|
||||||
? 'bg-background-deep/80 backdrop-blur-md border-b border-neon/30 shadow-[0_0_20px_rgba(0,0,0,0.5)]'
|
|
||||||
: 'bg-transparent border-b border-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="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">
|
<div className="flex items-center justify-between h-16">
|
||||||
{/* Logo with animation */}
|
{/* Logo */}
|
||||||
<Link to="/" className="flex items-center space-x-3">
|
<Link to="/" className="flex items-center space-x-3">
|
||||||
<motion.div
|
<div className="w-12 h-12 flex items-center justify-center overflow-visible">
|
||||||
className="w-12 h-12 flex items-center justify-center overflow-visible"
|
|
||||||
whileHover={{ rotate: 360 }}
|
|
||||||
transition={{ duration: 0.6, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src="/logo_bayarea.svg"
|
src="/logo_bayarea.svg"
|
||||||
alt="Bay Area Affiliates Logo"
|
alt="Bay Area Affiliates Logo"
|
||||||
className="w-10 h-10 opacity-90"
|
className="w-10 h-10 opacity-90 hover:opacity-100 transition-opacity duration-300"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</div>
|
||||||
<span className="font-heading font-bold text-lg text-white">
|
<span className="font-heading font-bold text-lg text-white">
|
||||||
Bay Area Affiliates
|
Bay Area Affiliates
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation with animations */}
|
{/* Desktop Navigation */}
|
||||||
<div className="hidden md:flex items-center space-x-8">
|
<div className="hidden md:flex items-center space-x-8">
|
||||||
{navItems.map((item, index) => (
|
{navItems.map((item) => (
|
||||||
<motion.div
|
|
||||||
key={item.name}
|
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
|
key={item.name}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
className={`font-medium transition-all duration-200 relative group px-2 py-1 ${isActive(item.path)
|
className={`font-medium transition-colors duration-200 ${isActive(item.path)
|
||||||
? 'text-neon'
|
? 'text-neon'
|
||||||
: 'text-white hover:text-neon'
|
: 'text-white hover:text-neon'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.name}
|
{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>
|
</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
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="btn-neon ml-4"
|
className="btn-neon ml-4"
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu button with animation */}
|
{/* Mobile menu button */}
|
||||||
<motion.button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="md:hidden text-white hover:text-neon transition-colors"
|
className="md:hidden text-white hover:text-neon transition-colors"
|
||||||
aria-label="Toggle navigation menu"
|
aria-label="Toggle navigation menu"
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="wait">
|
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||||
{isOpen ? (
|
</button>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Navigation with smooth animations */}
|
{/* Mobile Navigation */}
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<motion.div
|
<div className="md:hidden bg-white/5 backdrop-blur-2xl border-t border-white/20">
|
||||||
initial={{ height: 0, opacity: 0 }}
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||||
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) => (
|
{navItems.map((item) => (
|
||||||
<motion.div key={item.name} variants={staggerItem}>
|
|
||||||
<Link
|
<Link
|
||||||
|
key={item.name}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${isActive(item.path)
|
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${isActive(item.path)
|
||||||
|
|
@ -178,9 +95,7 @@ const Navigation = () => {
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
))}
|
))}
|
||||||
<motion.div variants={staggerItem}>
|
|
||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
|
|
@ -188,14 +103,11 @@ const Navigation = () => {
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.nav>
|
</div>
|
||||||
</>
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,47 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useLayoutEffect, useRef } from 'react';
|
||||||
import { motion, useInView } from 'framer-motion';
|
import gsap from 'gsap';
|
||||||
import React, { useRef } from 'react';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
|
|
||||||
interface ScrollRevealProps {
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
children: React.ReactNode;
|
|
||||||
width?: 'fit-content' | '100%';
|
type ScrollRevealProps = {
|
||||||
|
children: ReactNode;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
duration?: number;
|
|
||||||
direction?: 'up' | 'down' | 'left' | 'right';
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ScrollReveal: React.FC<ScrollRevealProps> = ({
|
const ScrollReveal = ({ children, delay = 0, className = '' }: ScrollRevealProps) => {
|
||||||
children,
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
width = 'fit-content',
|
|
||||||
delay = 0,
|
|
||||||
duration = 0.5,
|
|
||||||
direction = 'up',
|
|
||||||
className = ""
|
|
||||||
}) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
const isInView = useInView(ref, { once: true, margin: "-50px 0px" }); // Trigger slightly before element is fully in view
|
|
||||||
|
|
||||||
const getDirectionOffset = () => {
|
useLayoutEffect(() => {
|
||||||
switch (direction) {
|
const element = elementRef.current;
|
||||||
case 'up': return { y: 75 };
|
if (!element) return;
|
||||||
case 'down': return { y: -75 };
|
|
||||||
case 'left': return { x: 75 };
|
const ctx = gsap.context(() => {
|
||||||
case 'right': return { x: -75 };
|
gsap.fromTo(
|
||||||
default: return { y: 75 };
|
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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} style={{ position: 'relative', width, overflow: 'hidden' }} className={className}>
|
<div ref={elementRef} className={className}>
|
||||||
<motion.div
|
|
||||||
variants={{
|
|
||||||
hidden: { opacity: 0, ...getDirectionOffset() },
|
|
||||||
visible: { opacity: 1, x: 0, y: 0 },
|
|
||||||
}}
|
|
||||||
initial="hidden"
|
|
||||||
animate={isInView ? "visible" : "hidden"}
|
|
||||||
transition={{ duration, delay, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,244 +0,0 @@
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
const BackgroundAnimations = () => {
|
|
||||||
// Particles with better visibility (increased opacity and size)
|
|
||||||
const particles = useMemo(() => {
|
|
||||||
return Array.from({ length: 18 }, (_, i) => ({
|
|
||||||
id: i,
|
|
||||||
x: Math.random() * 100,
|
|
||||||
y: Math.random() * 100,
|
|
||||||
size: Math.random() * 4 + 2, // Larger particles (2-6px instead of 1-3px)
|
|
||||||
duration: Math.random() * 10 + 6,
|
|
||||||
delay: Math.random() * 4,
|
|
||||||
opacity: Math.random() * 0.4 + 0.5, // Much higher opacity (0.5-0.9)
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Circuit nodes for tech effect
|
|
||||||
const circuitNodes = useMemo(() => {
|
|
||||||
return Array.from({ length: 8 }, (_, i) => ({
|
|
||||||
id: i,
|
|
||||||
x: Math.random() * 80 + 10,
|
|
||||||
y: Math.random() * 80 + 10,
|
|
||||||
pulseDelay: Math.random() * 2,
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Connection lines between nodes
|
|
||||||
const connectionLines = useMemo(() => {
|
|
||||||
const lines = [];
|
|
||||||
for (let i = 0; i < circuitNodes.length - 1; i++) {
|
|
||||||
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' }}>
|
|
||||||
{/* Animated Grid Background */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `
|
|
||||||
linear-gradient(to right, rgba(51, 102, 255, 0.08) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(51, 102, 255, 0.08) 1px, transparent 1px)
|
|
||||||
`,
|
|
||||||
backgroundSize: '60px 60px',
|
|
||||||
willChange: 'transform',
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: [0.3, 0.5, 0.3],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 4,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* SVG Container for Lines and Nodes */}
|
|
||||||
<svg className="absolute inset-0 w-full h-full" style={{ willChange: 'transform' }}>
|
|
||||||
|
|
||||||
{/* Animated Connection Lines */}
|
|
||||||
{connectionLines.map((line) => (
|
|
||||||
<motion.line
|
|
||||||
key={line.id}
|
|
||||||
x1={`${line.x1}%`}
|
|
||||||
y1={`${line.y1}%`}
|
|
||||||
x2={`${line.x2}%`}
|
|
||||||
y2={`${line.y2}%`}
|
|
||||||
stroke="rgba(51, 102, 255, 0.4)"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
initial={{ pathLength: 0, opacity: 0 }}
|
|
||||||
animate={{ pathLength: 1, opacity: [0.3, 0.7, 0.3] }}
|
|
||||||
transition={{
|
|
||||||
pathLength: { duration: 2, delay: line.id * 0.3 },
|
|
||||||
opacity: { duration: 3, repeat: Infinity, ease: "easeInOut" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Vertical Data Streams */}
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<motion.line
|
|
||||||
key={`vertical-${i}`}
|
|
||||||
x1={`${10 + i * 18}%`}
|
|
||||||
y1="0%"
|
|
||||||
x2={`${10 + i * 18}%`}
|
|
||||||
y2="100%"
|
|
||||||
stroke="rgba(51, 102, 255, 0.3)"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeDasharray="8 16"
|
|
||||||
initial={{ strokeDashoffset: 0 }}
|
|
||||||
animate={{ strokeDashoffset: 100 }}
|
|
||||||
transition={{
|
|
||||||
duration: 3,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "linear",
|
|
||||||
delay: i * 0.4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Circuit Nodes with pulse ring */}
|
|
||||||
{circuitNodes.map((node) => (
|
|
||||||
<g key={node.id}>
|
|
||||||
{/* Pulse ring */}
|
|
||||||
<motion.circle
|
|
||||||
cx={`${node.x}%`}
|
|
||||||
cy={`${node.y}%`}
|
|
||||||
r="6"
|
|
||||||
fill="none"
|
|
||||||
stroke="#3366ff"
|
|
||||||
strokeWidth="1"
|
|
||||||
animate={{
|
|
||||||
r: [6, 12, 6],
|
|
||||||
opacity: [0.8, 0, 0.8],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2.5,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: node.pulseDelay,
|
|
||||||
ease: "easeOut",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* Core node */}
|
|
||||||
<motion.circle
|
|
||||||
cx={`${node.x}%`}
|
|
||||||
cy={`${node.y}%`}
|
|
||||||
r="4"
|
|
||||||
fill="#3366ff"
|
|
||||||
animate={{
|
|
||||||
opacity: [0.7, 1, 0.7],
|
|
||||||
r: [3, 4.5, 3],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: node.pulseDelay,
|
|
||||||
ease: "easeInOut",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Floating Data Particles - highly visible */}
|
|
||||||
{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}%`,
|
|
||||||
boxShadow: '0 0 12px rgba(51, 102, 255, 0.9), 0 0 24px rgba(51, 102, 255, 0.5)',
|
|
||||||
willChange: 'transform, opacity',
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
y: [0, -500],
|
|
||||||
opacity: [0, particle.opacity, particle.opacity, 0],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: particle.duration,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: particle.delay,
|
|
||||||
ease: "linear",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Scanline Effect */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
background: `repeating-linear-gradient(
|
|
||||||
0deg,
|
|
||||||
transparent,
|
|
||||||
transparent 2px,
|
|
||||||
rgba(51, 102, 255, 0.04) 2px,
|
|
||||||
rgba(51, 102, 255, 0.04) 4px
|
|
||||||
)`,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: [0.3, 0.5, 0.3],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Radial Glows */}
|
|
||||||
{[
|
|
||||||
{ x: 20, y: 30, size: 300 },
|
|
||||||
{ x: 80, y: 70, size: 350 },
|
|
||||||
{ x: 50, y: 50, size: 400 },
|
|
||||||
].map((pos, i) => (
|
|
||||||
<motion.div
|
|
||||||
key={`glow-${i}`}
|
|
||||||
className="absolute rounded-full pointer-events-none"
|
|
||||||
style={{
|
|
||||||
left: `${pos.x}%`,
|
|
||||||
top: `${pos.y}%`,
|
|
||||||
width: `${pos.size}px`,
|
|
||||||
height: `${pos.size}px`,
|
|
||||||
background: 'radial-gradient(circle, rgba(51, 102, 255, 0.25) 0%, transparent 70%)',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
willChange: 'transform',
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
scale: [1, 1.3, 1],
|
|
||||||
opacity: [0.5, 0.8, 0.5],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 5,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: i * 1.5,
|
|
||||||
ease: "easeInOut",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Corner Accent Glows */}
|
|
||||||
<div className="absolute top-0 left-0 w-96 h-96 bg-gradient-to-br from-neon/20 to-transparent rounded-full opacity-60 pointer-events-none" />
|
|
||||||
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-gradient-to-tl from-neon/20 to-transparent rounded-full opacity-60 pointer-events-none" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BackgroundAnimations;
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { ArrowRight, Clock, DollarSign, Phone } from 'lucide-react';
|
import { ArrowRight, Clock, DollarSign, Phone } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { motion, useInView } from 'framer-motion';
|
import ScrollReveal from '../ScrollReveal';
|
||||||
import { useRef } from 'react';
|
|
||||||
import { fadeInUp, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations';
|
|
||||||
|
|
||||||
const CTASection = () => {
|
const CTASection = () => {
|
||||||
const faqs = [
|
const faqs = [
|
||||||
|
|
@ -23,13 +21,6 @@ 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 (
|
return (
|
||||||
<section className="py-24 bg-background-deep relative overflow-hidden">
|
<section className="py-24 bg-background-deep relative overflow-hidden">
|
||||||
{/* Background decoration */}
|
{/* Background decoration */}
|
||||||
|
|
@ -38,96 +29,49 @@ const CTASection = () => {
|
||||||
|
|
||||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<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">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<motion.div
|
<ScrollReveal>
|
||||||
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">
|
<h2 className="font-heading font-bold text-4xl sm:text-5xl lg:text-6xl text-foreground mb-6">
|
||||||
Ready for <motion.span
|
Ready for <span className="text-neon text-glow">reliable IT?</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>
|
</h2>
|
||||||
|
|
||||||
<p className="text-xl text-foreground-muted mb-12 leading-relaxed">
|
<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.
|
Get started with a free 20-minute assessment.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Primary CTAs */}
|
{/* Primary CTAs */}
|
||||||
<motion.div
|
<ScrollReveal delay={200}>
|
||||||
ref={ctaRef}
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16">
|
||||||
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
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="btn-neon group flex items-center space-x-2 text-lg px-8 py-4"
|
className="btn-neon group flex items-center space-x-2 text-lg px-8 py-4"
|
||||||
>
|
>
|
||||||
<span>Book a 20-minute assessment</span>
|
<span>Book a 20-minute assessment</span>
|
||||||
<motion.div
|
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
|
||||||
animate={{ x: [0, 3, 0] }}
|
|
||||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<ArrowRight className="w-5 h-5" />
|
|
||||||
</motion.div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div whileHover={buttonHover} whileTap={buttonTap}>
|
|
||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="btn-ghost group flex items-center space-x-2 text-lg px-8 py-4"
|
className="btn-ghost group flex items-center space-x-2 text-lg px-8 py-4"
|
||||||
>
|
>
|
||||||
<span>Send a message</span>
|
<span>Send a message</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Micro FAQ */}
|
{/* Micro FAQ */}
|
||||||
<motion.div
|
<ScrollReveal delay={400}>
|
||||||
ref={faqRef}
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
initial="hidden"
|
|
||||||
animate={isFaqInView ? "visible" : "hidden"}
|
|
||||||
variants={staggerContainer}
|
|
||||||
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
|
||||||
>
|
|
||||||
{faqs.map((faq, index) => {
|
{faqs.map((faq, index) => {
|
||||||
const Icon = faq.icon;
|
const Icon = faq.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div key={faq.question} className="text-left">
|
||||||
key={faq.question}
|
<div className="flex items-start space-x-3">
|
||||||
variants={staggerItem}
|
<div className="w-8 h-8 bg-neon/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||||
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" />
|
<Icon className="w-4 h-4 text-neon" />
|
||||||
</motion.div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-foreground mb-2">
|
<h3 className="font-semibold text-foreground mb-2">
|
||||||
{faq.question}
|
{faq.question}
|
||||||
|
|
@ -136,43 +80,37 @@ const CTASection = () => {
|
||||||
{faq.answer}
|
{faq.answer}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</motion.div>
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Contact info */}
|
{/* Contact info */}
|
||||||
<motion.div
|
<ScrollReveal delay={600}>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<div className="mt-16 pt-8 border-t border-border/30">
|
||||||
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">
|
<p className="text-sm text-foreground-muted mb-4">
|
||||||
Ready to talk? We're here to help.
|
Ready to talk? We're here to help.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center text-sm">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center text-sm">
|
||||||
<motion.a
|
<a
|
||||||
href="tel:+1-361-555-0123"
|
href="tel:+1-361-555-0123"
|
||||||
className="text-neon hover:text-neon/80 transition-colors flex items-center"
|
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" />
|
<Phone className="w-4 h-4 mr-2" />
|
||||||
(361) 555-0123
|
(361) 555-0123
|
||||||
</motion.a>
|
</a>
|
||||||
<span className="hidden sm:block text-border">|</span>
|
<span className="hidden sm:block text-border">|</span>
|
||||||
<motion.a
|
<a
|
||||||
href="mailto:info@bayareaaffiliates.com"
|
href="mailto:info@bayareaaffiliates.com"
|
||||||
className="text-neon hover:text-neon/80 transition-colors"
|
className="text-neon hover:text-neon/80 transition-colors"
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
>
|
||||||
info@bayareaaffiliates.com
|
info@bayareaaffiliates.com
|
||||||
</motion.a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,165 +1,87 @@
|
||||||
import { ArrowRight, Play } from 'lucide-react';
|
import { ArrowRight, Play } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
import { useEffect, useRef } from 'react';
|
||||||
import { heroVariants, heroItemVariants, buttonHover, buttonTap, easing } from '@/utils/animations';
|
import heroNetwork from '@/assets/hero-network.jpg';
|
||||||
import BackgroundAnimations from './BackgroundAnimations';
|
|
||||||
|
|
||||||
const HeroSection = () => {
|
const HeroSection = () => {
|
||||||
const { scrollY } = useScroll();
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
// Smooth parallax effect with Framer Motion
|
useEffect(() => {
|
||||||
const y = useTransform(scrollY, [0, 500], [0, 150]);
|
const handleScroll = () => {
|
||||||
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
|
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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="section-pin">
|
<section className="section-pin">
|
||||||
<div className="relative h-full flex items-center justify-center overflow-hidden">
|
<div className="relative h-full flex items-center justify-center overflow-hidden">
|
||||||
{/* Background image with smooth parallax */}
|
{/* Background image with parallax */}
|
||||||
<motion.div
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
className="absolute inset-0 overflow-hidden"
|
<img
|
||||||
style={{ y }}
|
ref={imageRef}
|
||||||
>
|
|
||||||
<picture>
|
|
||||||
<source type="image/avif" srcSet="/serverroom.avif" />
|
|
||||||
<source type="image/webp" srcSet="/serverroom.webp" />
|
|
||||||
<motion.img
|
|
||||||
src="/serverroom.png"
|
src="/serverroom.png"
|
||||||
alt="Modern IT infrastructure with network connections and server equipment"
|
alt="Modern IT infrastructure with network connections"
|
||||||
className="w-full h-[110%] object-cover"
|
className="w-full h-[110%] object-cover will-change-transform"
|
||||||
initial={{ scale: 1.1, opacity: 0 }}
|
style={{ transform: 'translateY(0px) scale(1.1)' }}
|
||||||
animate={{ scale: 1.1, opacity: 1 }}
|
|
||||||
transition={{ duration: 1.2, ease: easing.elegant }}
|
|
||||||
loading="eager"
|
|
||||||
fetchPriority="high"
|
|
||||||
/>
|
/>
|
||||||
</picture>
|
{/* Dark overlay */}
|
||||||
|
<div className="absolute inset-0 bg-black/35"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Animated background effects */}
|
{/* Main content */}
|
||||||
<BackgroundAnimations />
|
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
|
||||||
{/* 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">
|
<div className="max-w-4xl mx-auto">
|
||||||
|
|
||||||
{/* Badge */}
|
{/* Badge */}
|
||||||
<motion.div
|
<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)]">
|
||||||
variants={heroItemVariants}
|
<span className="w-2 h-2 bg-neon rounded-full mr-2 animate-glow-pulse"></span>
|
||||||
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
|
Serving the Coastal Bend since 2010
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Main headline */}
|
{/* Main headline */}
|
||||||
<motion.h1
|
<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)]">
|
||||||
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{' '}
|
Modern IT that keeps your{' '}
|
||||||
<motion.span
|
<span className="text-neon text-glow drop-shadow-[0_0_30px_rgba(51,102,255,0.8)]">business moving</span>
|
||||||
className="text-neon text-glow drop-shadow-[0_0_30px_rgba(51,102,255,0.8)]"
|
</h1>
|
||||||
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 */}
|
{/* Subheadline */}
|
||||||
<motion.p
|
<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)]">
|
||||||
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,
|
From fast devices to secure remote access and resilient networks — we design,
|
||||||
run and protect your tech so you can focus on growth.
|
run and protect your tech so you can focus on growth.
|
||||||
</motion.p>
|
</p>
|
||||||
|
|
||||||
{/* CTA buttons with hover animations */}
|
{/* CTA buttons */}
|
||||||
<motion.div
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||||
variants={heroItemVariants}
|
|
||||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
whileHover={buttonHover}
|
|
||||||
whileTap={buttonTap}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="btn-neon group flex items-center space-x-2"
|
className="btn-neon group flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
<span>Get in touch</span>
|
<span>Get in touch</span>
|
||||||
<motion.div
|
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
|
||||||
animate={{ x: [0, 3, 0] }}
|
|
||||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<ArrowRight className="w-5 h-5" />
|
|
||||||
</motion.div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<button className="btn-ghost group flex items-center space-x-2">
|
||||||
whileHover={buttonHover}
|
<Play className="w-5 h-5 transition-transform group-hover:scale-110" />
|
||||||
whileTap={buttonTap}
|
<span>See our system</span>
|
||||||
>
|
</button>
|
||||||
<Link
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Animated scroll indicator */}
|
{/* Scroll indicator */}
|
||||||
<motion.div
|
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2">
|
||||||
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-6 h-10 border-2 border-neon/60 rounded-full flex justify-center drop-shadow-[0_0_10px_rgba(51,102,255,0.3)]">
|
||||||
<motion.div
|
<div className="w-1 h-3 bg-neon rounded-full mt-2 animate-bounce"></div>
|
||||||
className="w-1 h-3 bg-neon rounded-full mt-2"
|
</div>
|
||||||
animate={{ y: [0, 12, 0] }}
|
|
||||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,10 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Search, Shield, Cog, Zap } from 'lucide-react';
|
import { Search, Shield, Cog, Zap } from 'lucide-react';
|
||||||
import { motion, useInView, useScroll, useTransform } from 'framer-motion';
|
import ScrollReveal from '../ScrollReveal';
|
||||||
import { fadeInUp, scaleIn } from '@/utils/animations';
|
|
||||||
|
|
||||||
const ProcessTimeline = () => {
|
const ProcessTimeline = () => {
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const sectionRef = useRef<HTMLDivElement>(null);
|
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 = [
|
const steps = [
|
||||||
{
|
{
|
||||||
|
|
@ -73,12 +63,7 @@ const ProcessTimeline = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<motion.div
|
<ScrollReveal>
|
||||||
ref={headerRef}
|
|
||||||
initial="hidden"
|
|
||||||
animate={isHeaderInView ? "visible" : "hidden"}
|
|
||||||
variants={fadeInUp}
|
|
||||||
>
|
|
||||||
<div className="text-center mb-20">
|
<div className="text-center mb-20">
|
||||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
<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
|
How we <span className="text-neon">transform</span> your IT
|
||||||
|
|
@ -87,14 +72,14 @@ const ProcessTimeline = () => {
|
||||||
Our proven four-phase methodology ensures systematic improvement and lasting results.
|
Our proven four-phase methodology ensures systematic improvement and lasting results.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Timeline line */}
|
{/* 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 left-8 lg:left-1/2 lg:transform lg:-translate-x-1/2 top-0 bottom-0 w-px bg-border">
|
||||||
<motion.div
|
<div
|
||||||
className="absolute top-0 left-0 w-full bg-neon"
|
className="absolute top-0 left-0 w-full bg-neon transition-all duration-500 ease-out"
|
||||||
style={{ height: useTransform(timelineProgress, (value) => `${value}%`) }}
|
style={{ height: `${(activeStep + 1) * 25}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -104,35 +89,17 @@ const ProcessTimeline = () => {
|
||||||
const Icon = step.icon;
|
const Icon = step.icon;
|
||||||
const isActive = index <= activeStep;
|
const isActive = index <= activeStep;
|
||||||
const isEven = index % 2 === 0;
|
const isEven = index % 2 === 0;
|
||||||
const stepRef = useRef(null);
|
|
||||||
const isInView = useInView(stepRef, { once: true, margin: "-150px" });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<ScrollReveal key={step.title} delay={index * 100}>
|
||||||
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 ${
|
<div className={`relative flex flex-col lg:flex-row items-center ${
|
||||||
isEven ? '' : 'lg:flex-row-reverse'
|
isEven ? '' : 'lg:flex-row-reverse'
|
||||||
}`}>
|
}`}>
|
||||||
{/* Step content */}
|
{/* Step content */}
|
||||||
<motion.div
|
<div className={`flex-1 ${isEven ? 'lg:pr-16' : 'lg:pl-16'} ${
|
||||||
className={`flex-1 ${isEven ? 'lg:pr-16' : 'lg:pl-16'} ${
|
|
||||||
isEven ? 'lg:text-right' : 'lg:text-left'
|
isEven ? 'lg:text-right' : 'lg:text-left'
|
||||||
} text-center lg:text-left`}
|
} text-center lg:text-left`}>
|
||||||
initial={{ opacity: 0, x: isEven ? -40 : 40 }}
|
<div className="card-dark p-8 max-w-lg mx-auto lg:mx-0">
|
||||||
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">
|
<div className="mb-4">
|
||||||
<span className="text-sm font-medium text-neon uppercase tracking-wider">
|
<span className="text-sm font-medium text-neon uppercase tracking-wider">
|
||||||
Step {index + 1}
|
Step {index + 1}
|
||||||
|
|
@ -147,34 +114,24 @@ const ProcessTimeline = () => {
|
||||||
<p className="text-sm text-foreground-muted">
|
<p className="text-sm text-foreground-muted">
|
||||||
{step.details}
|
{step.details}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Timeline dot */}
|
{/* Timeline dot */}
|
||||||
<div className="relative z-10 my-8 lg:my-0">
|
<div className="relative z-10 my-8 lg:my-0">
|
||||||
<motion.div
|
<div className={`w-16 h-16 rounded-full border-4 flex items-center justify-center transition-all duration-500 ${
|
||||||
className={`w-16 h-16 rounded-full border-4 flex items-center justify-center ${
|
|
||||||
isActive
|
isActive
|
||||||
? 'border-neon bg-neon text-neon-foreground shadow-neon'
|
? 'border-neon bg-neon text-neon-foreground shadow-neon'
|
||||||
: 'border-border bg-background text-foreground-muted'
|
: '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" />
|
<Icon className="w-6 h-6" />
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spacer for layout */}
|
{/* Spacer for layout */}
|
||||||
<div className="flex-1 hidden lg:block"></div>
|
<div className="flex-1 hidden lg:block"></div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { MapPin, Star, Users } from 'lucide-react';
|
import { MapPin, Star, Users } from 'lucide-react';
|
||||||
import { motion, useInView } from 'framer-motion';
|
import ScrollReveal from '../ScrollReveal';
|
||||||
import { useRef } from 'react';
|
|
||||||
import CountUpNumber from '../CountUpNumber';
|
import CountUpNumber from '../CountUpNumber';
|
||||||
import { fadeInUp, scaleIn, staggerContainer, staggerItem } from '@/utils/animations';
|
|
||||||
|
|
||||||
const ProofSection = () => {
|
const ProofSection = () => {
|
||||||
const stats = [
|
const stats = [
|
||||||
|
|
@ -19,13 +17,6 @@ const ProofSection = () => {
|
||||||
company: "Coastal Medical Group"
|
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 (
|
return (
|
||||||
<section className="py-24 bg-background relative overflow-hidden">
|
<section className="py-24 bg-background relative overflow-hidden">
|
||||||
{/* Background decoration */}
|
{/* Background decoration */}
|
||||||
|
|
@ -33,27 +24,12 @@ 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="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">
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<motion.div
|
<ScrollReveal>
|
||||||
ref={headerRef}
|
|
||||||
initial="hidden"
|
|
||||||
animate={isHeaderInView ? "visible" : "hidden"}
|
|
||||||
variants={fadeInUp}
|
|
||||||
>
|
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<motion.div
|
<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">
|
||||||
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" />
|
<MapPin className="w-4 h-4 mr-2" />
|
||||||
Proudly serving the Coastal Bend
|
Proudly serving the Coastal Bend
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||||
Local expertise for{' '}
|
Local expertise for{' '}
|
||||||
|
|
@ -66,22 +42,13 @@ const ProofSection = () => {
|
||||||
tailored solutions that work in the real world.
|
tailored solutions that work in the real world.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<motion.div
|
<ScrollReveal delay={200}>
|
||||||
ref={statsRef}
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-20">
|
||||||
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) => (
|
{stats.map((stat, index) => (
|
||||||
<motion.div
|
<div key={stat.label} className="text-center">
|
||||||
key={stat.label}
|
|
||||||
variants={staggerItem}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<div className="font-heading font-bold text-4xl lg:text-5xl text-neon mb-2">
|
<div className="font-heading font-bold text-4xl lg:text-5xl text-neon mb-2">
|
||||||
<CountUpNumber
|
<CountUpNumber
|
||||||
value={stat.value}
|
value={stat.value}
|
||||||
|
|
@ -89,69 +56,34 @@ const ProofSection = () => {
|
||||||
className="inline-block"
|
className="inline-block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<div className="text-foreground-muted text-sm lg:text-base">
|
||||||
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}
|
{stat.label}
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Testimonial */}
|
{/* Testimonial */}
|
||||||
<motion.div
|
<ScrollReveal delay={400}>
|
||||||
ref={testimonialRef}
|
<div className="card-dark p-8 lg:p-12 max-w-4xl mx-auto">
|
||||||
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">
|
<div className="flex flex-col lg:flex-row items-start gap-8">
|
||||||
{/* Quote */}
|
{/* Quote */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<motion.div
|
<div className="flex items-center mb-6">
|
||||||
className="flex items-center mb-6"
|
|
||||||
initial="hidden"
|
|
||||||
animate={isTestimonialInView ? "visible" : "hidden"}
|
|
||||||
variants={staggerContainer}
|
|
||||||
>
|
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<motion.div
|
<Star key={i} className="w-5 h-5 text-neon fill-current" />
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<blockquote className="text-lg lg:text-xl text-foreground leading-relaxed mb-6">
|
<blockquote className="text-lg lg:text-xl text-foreground leading-relaxed mb-6">
|
||||||
"{testimonial.quote}"
|
"{testimonial.quote}"
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<motion.div
|
<div className="w-12 h-12 bg-neon/20 rounded-full flex items-center justify-center mr-4">
|
||||||
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" />
|
<Users className="w-6 h-6 text-neon" />
|
||||||
</motion.div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-foreground">{testimonial.author}</div>
|
<div className="font-semibold text-foreground">{testimonial.author}</div>
|
||||||
<div className="text-foreground-muted text-sm">
|
<div className="text-foreground-muted text-sm">
|
||||||
|
|
@ -161,43 +93,29 @@ const ProofSection = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Service area */}
|
{/* Service area */}
|
||||||
<motion.div
|
<ScrollReveal delay={600}>
|
||||||
initial={{ opacity: 0, y: 30 }}
|
<div className="mt-16 text-center">
|
||||||
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">
|
<h3 className="font-heading font-semibold text-xl text-foreground mb-6">
|
||||||
Serving businesses throughout the Coastal Bend
|
Serving businesses throughout the Coastal Bend
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<motion.div
|
<div className="flex flex-wrap justify-center items-center gap-6 text-foreground-muted">
|
||||||
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',
|
'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass',
|
||||||
'Rockport', 'Fulton', 'Sinton', 'Mathis'
|
'Rockport', 'Fulton', 'Sinton', 'Mathis'
|
||||||
].map((city) => (
|
].map((city) => (
|
||||||
<motion.span
|
<span key={city} className="flex items-center text-sm">
|
||||||
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" />
|
<MapPin className="w-3 h-3 mr-1 text-neon" />
|
||||||
{city}
|
{city}
|
||||||
</motion.span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { Monitor, Wifi, Cloud, Shield, Database, Settings } from 'lucide-react';
|
import { Monitor, Wifi, Cloud, Shield, Database, Settings } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { motion, useInView } from 'framer-motion';
|
import ScrollReveal from '../ScrollReveal';
|
||||||
import { useRef } from 'react';
|
|
||||||
import { fadeInUp, staggerContainer, staggerItem, cardHover, glowHover } from '@/utils/animations';
|
|
||||||
|
|
||||||
const ServicesOverview = () => {
|
const ServicesOverview = () => {
|
||||||
const services = [
|
const services = [
|
||||||
|
|
@ -44,11 +42,6 @@ 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 (
|
return (
|
||||||
<section className="py-24 bg-background-deep relative overflow-hidden">
|
<section className="py-24 bg-background-deep relative overflow-hidden">
|
||||||
{/* Background decoration */}
|
{/* Background decoration */}
|
||||||
|
|
@ -57,12 +50,7 @@ const ServicesOverview = () => {
|
||||||
<div className="absolute bottom-1/4 left-0 w-96 h-96 bg-neon/5 rounded-full blur-3xl"></div>
|
<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">
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<motion.div
|
<ScrollReveal>
|
||||||
ref={headerRef}
|
|
||||||
initial="hidden"
|
|
||||||
animate={isHeaderInView ? "visible" : "hidden"}
|
|
||||||
variants={fadeInUp}
|
|
||||||
>
|
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||||
Complete IT solutions for{' '}
|
Complete IT solutions for{' '}
|
||||||
|
|
@ -72,44 +60,22 @@ const ServicesOverview = () => {
|
||||||
From desktop support to enterprise infrastructure — we've got your technology covered.
|
From desktop support to enterprise infrastructure — we've got your technology covered.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
<motion.div
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
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) => {
|
{services.map((service, index) => {
|
||||||
const Icon = service.icon;
|
const Icon = service.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div key={service.title} variants={staggerItem} className="h-full">
|
<ScrollReveal key={service.title} delay={index * 100}>
|
||||||
<motion.div
|
<div className="card-dark p-8 group hover:shadow-neon transition-all duration-500 hover:-translate-y-1">
|
||||||
className="card-dark p-8 h-full relative overflow-hidden group border border-white/5 hover:border-neon/50 transition-colors duration-500"
|
|
||||||
whileHover={{
|
|
||||||
y: -10,
|
|
||||||
scale: 1.02,
|
|
||||||
boxShadow: "0 20px 40px -10px rgba(51, 102, 255, 0.3)"
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
{/* Subtle gradient background nice touch */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-neon/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
||||||
|
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<motion.div
|
<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">
|
||||||
className="w-12 h-12 bg-neon/10 rounded-xl flex items-center justify-center mb-6 relative z-10 group-hover:bg-neon/20 transition-colors duration-300"
|
<Icon className="w-6 h-6 text-neon" />
|
||||||
whileHover={{ rotate: 360, scale: 1.1 }}
|
</div>
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
>
|
|
||||||
<Icon className="w-6 h-6 text-neon drop-shadow-[0_0_8px_rgba(51,102,255,0.6)]" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="relative z-10">
|
<h3 className="font-heading font-bold text-xl text-foreground mb-4">
|
||||||
<h3 className="font-heading font-bold text-xl text-foreground mb-4 group-hover:text-neon transition-colors duration-300">
|
|
||||||
{service.title}
|
{service.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
|
@ -121,18 +87,7 @@ const ServicesOverview = () => {
|
||||||
<ul className="space-y-2 mb-6">
|
<ul className="space-y-2 mb-6">
|
||||||
{service.features.map((feature) => (
|
{service.features.map((feature) => (
|
||||||
<li key={feature} className="flex items-center text-sm text-foreground-muted">
|
<li key={feature} className="flex items-center text-sm text-foreground-muted">
|
||||||
<motion.div
|
<div className="w-1.5 h-1.5 bg-neon rounded-full mr-3"></div>
|
||||||
className="w-1.5 h-1.5 bg-neon rounded-full mr-3 shadow-[0_0_5px_rgba(51,102,255,0.8)]"
|
|
||||||
animate={{
|
|
||||||
scale: [1, 1.5, 1],
|
|
||||||
opacity: [0.7, 1, 0.7]
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: index * 0.2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{feature}
|
{feature}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -141,41 +96,30 @@ const ServicesOverview = () => {
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<Link
|
<Link
|
||||||
to="/services"
|
to="/services"
|
||||||
className="inline-flex items-center text-neon font-medium group-hover:translate-x-2 transition-transform duration-300"
|
className="inline-flex items-center text-neon font-medium hover:text-neon/80 transition-colors group-hover:underline"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
<motion.svg
|
<svg className="w-4 h-4 ml-1 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
||||||
</motion.svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
</motion.div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom CTA */}
|
{/* Bottom CTA */}
|
||||||
<motion.div
|
<ScrollReveal delay={600}>
|
||||||
initial={{ opacity: 0, y: 30 }}
|
<div className="text-center mt-16">
|
||||||
animate={isGridInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
|
||||||
transition={{ delay: 0.8, duration: 0.6 }}
|
|
||||||
className="text-center mt-16"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
to="/services"
|
to="/services"
|
||||||
className="btn-ghost text-lg px-12 py-4"
|
className="btn-ghost text-lg px-12 py-4"
|
||||||
>
|
>
|
||||||
View all services
|
View all services
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
const TrustedBy = () => {
|
|
||||||
// Placeholder logos - in a real app these would be SVGs
|
|
||||||
const companies = [
|
|
||||||
{ name: "TechCorp", color: "bg-blue-400" },
|
|
||||||
{ name: "SecureNet", color: "bg-indigo-400" },
|
|
||||||
{ name: "CloudSystems", color: "bg-cyan-400" },
|
|
||||||
{ name: "FutureData", color: "bg-sky-400" },
|
|
||||||
{ name: "CyberGuard", color: "bg-teal-400" },
|
|
||||||
{ name: "NetWorks", color: "bg-emerald-400" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// duplicate for seamless loop
|
|
||||||
const logos = [...companies, ...companies, ...companies];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="py-10 border-b border-white/5 bg-background-deep relative overflow-hidden">
|
|
||||||
<p className="text-center text-sm font-medium text-foreground-muted mb-8 tracking-wider uppercase relative z-10 px-4">
|
|
||||||
Trusted by innovative businesses in Corpus Christi
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="relative flex overflow-hidden mask-linear-fade">
|
|
||||||
{/* Gradient Masks for smooth fade out at edges */}
|
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-24 bg-gradient-to-r from-background-deep to-transparent z-10"></div>
|
|
||||||
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-background-deep to-transparent z-10"></div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="flex space-x-16 items-center"
|
|
||||||
animate={{ x: ["0%", "-50%"] }}
|
|
||||||
transition={{
|
|
||||||
duration: 30,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "linear",
|
|
||||||
repeatType: "loop"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{logos.map((company, index) => (
|
|
||||||
<div key={index} className="flex items-center space-x-2 opacity-50 grayscale hover:grayscale-0 hover:opacity-100 transition-all duration-300">
|
|
||||||
{/* Fake Logo Icon */}
|
|
||||||
<div className={`w-8 h-8 rounded-lg ${company.color}/20 flex items-center justify-center`}>
|
|
||||||
<div className={`w-4 h-4 rounded-full ${company.color}`}></div>
|
|
||||||
</div>
|
|
||||||
{/* Fake Logo Text */}
|
|
||||||
<span className="text-xl font-bold text-white tracking-tight">{company.name}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TrustedBy;
|
|
||||||
|
|
@ -1,58 +1,6 @@
|
||||||
import { Shield, Zap, Users, ArrowRight } from 'lucide-react';
|
import { Shield, Zap, Users, ArrowRight } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { motion, useInView, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
import ScrollReveal from '../ScrollReveal';
|
||||||
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 ValuePillars = () => {
|
||||||
const pillars = [
|
const pillars = [
|
||||||
|
|
@ -79,21 +27,13 @@ const ValuePillars = () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const headerRef = useRef(null);
|
|
||||||
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-background-deep relative overflow-hidden">
|
<section className="py-24 bg-background-deep relative overflow-hidden">
|
||||||
{/* Background decoration */}
|
{/* Background decoration */}
|
||||||
<div className="absolute inset-0 grid-overlay opacity-20"></div>
|
<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">
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<motion.div
|
<ScrollReveal>
|
||||||
ref={headerRef}
|
|
||||||
initial="hidden"
|
|
||||||
animate={isHeaderInView ? "visible" : "hidden"}
|
|
||||||
variants={fadeInUp}
|
|
||||||
>
|
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
<h2 className="font-heading font-bold text-4xl sm:text-5xl text-foreground mb-6">
|
||||||
Why teams choose us for{' '}
|
Why teams choose us for{' '}
|
||||||
|
|
@ -103,44 +43,25 @@ const ValuePillars = () => {
|
||||||
We handle the complexity so you can focus on what you do best.
|
We handle the complexity so you can focus on what you do best.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
|
|
||||||
<div className="space-y-24">
|
<div className="space-y-24">
|
||||||
{pillars.map((pillar, index) => {
|
{pillars.map((pillar, index) => {
|
||||||
const Icon = pillar.icon;
|
const Icon = pillar.icon;
|
||||||
const isReverse = index % 2 === 1;
|
const isReverse = index % 2 === 1;
|
||||||
const itemRef = useRef(null);
|
|
||||||
const isInView = useInView(itemRef, { once: true, margin: "-100px" });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<ScrollReveal key={pillar.number} delay={index * 200}>
|
||||||
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`}>
|
<div className={`flex flex-col ${isReverse ? 'lg:flex-row-reverse' : 'lg:flex-row'} items-center gap-12 lg:gap-16`}>
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 space-y-6">
|
<div className="flex-1 space-y-6">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<motion.span
|
<span className="text-6xl font-heading font-bold text-neon/30">
|
||||||
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}
|
{pillar.number}
|
||||||
</motion.span>
|
</span>
|
||||||
<motion.div
|
<div className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center">
|
||||||
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" />
|
<Icon className="w-6 h-6 text-neon" />
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="font-heading font-bold text-3xl text-foreground">
|
<h3 className="font-heading font-bold text-3xl text-foreground">
|
||||||
|
|
@ -151,48 +72,29 @@ const ValuePillars = () => {
|
||||||
{pillar.description}
|
{pillar.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<motion.div whileHover={buttonHover} whileTap={buttonTap}>
|
|
||||||
<Link
|
<Link
|
||||||
to="/services"
|
to="/services"
|
||||||
className="btn-ghost group flex items-center space-x-2 w-fit"
|
className="btn-ghost group flex items-center space-x-2 w-fit"
|
||||||
onClick={() => window.scrollTo(0, 0)}
|
onClick={() => window.scrollTo(0, 0)}
|
||||||
>
|
>
|
||||||
<span>Learn more</span>
|
<span>Learn more</span>
|
||||||
<motion.div
|
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
||||||
animate={{ x: [0, 3, 0] }}
|
|
||||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</motion.div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image with 3D Tilt Effect */}
|
{/* Image */}
|
||||||
<TiltCard className="flex-1">
|
<div className="flex-1 parallax">
|
||||||
<div className="card-dark p-2 group hover:shadow-neon transition-all duration-500">
|
<div className="card-dark p-2 group hover:shadow-neon transition-all duration-500">
|
||||||
<div className="relative overflow-hidden rounded-xl">
|
<img
|
||||||
<motion.img
|
|
||||||
src={pillar.image}
|
src={pillar.image}
|
||||||
alt={pillar.title}
|
alt={pillar.title}
|
||||||
className="w-full h-64 lg:h-80 object-cover"
|
className="w-full h-64 lg:h-80 object-cover rounded-xl transition-transform duration-500 group-hover:scale-105"
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
loading="lazy"
|
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>
|
</div>
|
||||||
</TiltCard>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</ScrollReveal>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
170
src/index.css
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
@ -86,31 +88,6 @@
|
||||||
scroll-behavior: smooth;
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
|
|
@ -124,29 +101,6 @@
|
||||||
transition-duration: 0.01ms !important;
|
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 {
|
@layer components {
|
||||||
|
|
@ -194,26 +148,6 @@
|
||||||
@apply rounded-[var(--radius)] px-8 py-4 font-semibold;
|
@apply rounded-[var(--radius)] px-8 py-4 font-semibold;
|
||||||
@apply transition-all duration-300 ease-out;
|
@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)];
|
@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 {
|
.btn-ghost {
|
||||||
|
|
@ -221,116 +155,16 @@
|
||||||
@apply rounded-[var(--radius)] px-8 py-4 font-semibold;
|
@apply rounded-[var(--radius)] px-8 py-4 font-semibold;
|
||||||
@apply transition-all duration-300 ease-out;
|
@apply transition-all duration-300 ease-out;
|
||||||
@apply hover:shadow-[0_0_15px_hsl(var(--neon)/0.3)];
|
@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 styles */
|
||||||
.card-dark {
|
.card-dark {
|
||||||
@apply bg-card border border-card-border rounded-[var(--radius-lg)];
|
@apply bg-card border border-card-border rounded-[var(--radius-lg)];
|
||||||
@apply backdrop-blur-sm shadow-[var(--shadow-card)];
|
@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 */
|
/* Typography helpers */
|
||||||
.text-balance {
|
.text-balance {
|
||||||
text-wrap: 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,19 +4,10 @@ import { Shield, Users, Zap, MapPin } from 'lucide-react';
|
||||||
import ScrollReveal from '@/components/ScrollReveal';
|
import ScrollReveal from '@/components/ScrollReveal';
|
||||||
import CountUpNumber from '@/components/CountUpNumber';
|
import CountUpNumber from '@/components/CountUpNumber';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useSEO } from '@/hooks/useSEO';
|
|
||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
const imageRef = useRef<HTMLImageElement>(null);
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
useSEO({
|
|
||||||
title: 'About Bay Area Affiliates | IT Experts Corpus Christi Since 2010',
|
|
||||||
description: 'Local IT expertise for the Coastal Bend since 2010. 150+ businesses served, 99.9% uptime. Security-first approach with clear communication. Meet the team.',
|
|
||||||
keywords: 'about Bay Area Affiliates, IT company Corpus Christi, managed services provider Texas, local IT support Coastal Bend',
|
|
||||||
canonical: 'https://bayarea-cc.com/about',
|
|
||||||
ogImage: 'https://bayarea-cc.com/og-image.png',
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (imageRef.current) {
|
if (imageRef.current) {
|
||||||
|
|
@ -75,7 +66,7 @@ const About = () => {
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero section with image background */}
|
{/* Hero section with image background */}
|
||||||
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
||||||
{/* Background image with parallax */}
|
{/* Background image with parallax */}
|
||||||
|
|
@ -87,7 +78,7 @@ const About = () => {
|
||||||
className="w-full h-[110%] object-cover will-change-transform"
|
className="w-full h-[110%] object-cover will-change-transform"
|
||||||
style={{ transform: 'translateY(0px) scale(1.1)' }}
|
style={{ transform: 'translateY(0px) scale(1.1)' }}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
fetchPriority="high"
|
fetchpriority="high"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const Blog = () => {
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero section with image background */}
|
{/* Hero section with image background */}
|
||||||
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
||||||
{/* Background image with parallax */}
|
{/* Background image with parallax */}
|
||||||
|
|
@ -93,7 +93,7 @@ const Blog = () => {
|
||||||
className="w-full h-[110%] object-cover will-change-transform"
|
className="w-full h-[110%] object-cover will-change-transform"
|
||||||
style={{ transform: 'translateY(0px) scale(1.1)' }}
|
style={{ transform: 'translateY(0px) scale(1.1)' }}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
fetchPriority="high"
|
fetchpriority="high"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -211,7 +211,8 @@ const Blog = () => {
|
||||||
{/* Scroll to Top Button */}
|
{/* Scroll to Top Button */}
|
||||||
<button
|
<button
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
className={`fixed bottom-8 right-8 z-50 p-4 bg-neon text-neon-foreground rounded-full shadow-lg shadow-neon/50 transition-all duration-300 hover:scale-110 hover:shadow-neon ${showScrollTop ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-16 pointer-events-none'
|
className={`fixed bottom-8 right-8 z-50 p-4 bg-neon text-neon-foreground rounded-full shadow-lg shadow-neon/50 transition-all duration-300 hover:scale-110 hover:shadow-neon ${
|
||||||
|
showScrollTop ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-16 pointer-events-none'
|
||||||
}`}
|
}`}
|
||||||
aria-label="Scroll to top"
|
aria-label="Scroll to top"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ const BlogPost = () => {
|
||||||
<div className="min-h-screen bg-background-deep">
|
<div className="min-h-screen bg-background-deep">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero section with featured image */}
|
{/* Hero section with featured image */}
|
||||||
<section className="relative h-[60vh] min-h-[400px] flex items-center justify-center overflow-hidden">
|
<section className="relative h-[60vh] min-h-[400px] flex items-center justify-center overflow-hidden">
|
||||||
{/* Background image */}
|
{/* Background image */}
|
||||||
|
|
@ -122,7 +122,7 @@ const BlogPost = () => {
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
fetchPriority="high"
|
fetchpriority="high"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-background-deep via-background-deep/60 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-background-deep via-background-deep/60 to-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,8 @@ import { Mail, Phone, MapPin, Clock, DollarSign, Headphones } from 'lucide-react
|
||||||
import ScrollReveal from '@/components/ScrollReveal';
|
import ScrollReveal from '@/components/ScrollReveal';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useSEO } from '@/hooks/useSEO';
|
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
useSEO({
|
|
||||||
title: 'Contact Bay Area Affiliates | Free IT Consultation Corpus Christi',
|
|
||||||
description: 'Get in touch for a free IT consultation. 24-hour response, free assessment, no obligation. Serving Corpus Christi, Portland, Rockport & the Coastal Bend.',
|
|
||||||
keywords: 'contact IT support Corpus Christi, free IT consultation, Bay Area Affiliates phone, IT services quote Texas',
|
|
||||||
canonical: 'https://bayarea-cc.com/contact',
|
|
||||||
ogImage: 'https://bayarea-cc.com/og-image.png',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const imageRef = useRef<HTMLImageElement>(null);
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|
@ -90,7 +81,7 @@ const Contact = () => {
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero section with image background */}
|
{/* Hero section with image background */}
|
||||||
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
||||||
{/* Background image with parallax */}
|
{/* Background image with parallax */}
|
||||||
|
|
@ -167,7 +158,7 @@ const Contact = () => {
|
||||||
required
|
required
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-3 bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-neon focus:bg-white/10 focus:shadow-[0_0_15px_rgba(51,102,255,0.3)] transition-all duration-300 text-foreground placeholder-foreground-muted/50"
|
className="w-full px-4 py-3 bg-input border border-input-border rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-transparent text-foreground"
|
||||||
placeholder="Your full name"
|
placeholder="Your full name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -183,7 +174,7 @@ const Contact = () => {
|
||||||
required
|
required
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-3 bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-neon focus:bg-white/10 focus:shadow-[0_0_15px_rgba(51,102,255,0.3)] transition-all duration-300 text-foreground placeholder-foreground-muted/50"
|
className="w-full px-4 py-3 bg-input border border-input-border rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-transparent text-foreground"
|
||||||
placeholder="your@email.com"
|
placeholder="your@email.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -200,7 +191,7 @@ const Contact = () => {
|
||||||
name="company"
|
name="company"
|
||||||
value={formData.company}
|
value={formData.company}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-3 bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-neon focus:bg-white/10 focus:shadow-[0_0_15px_rgba(51,102,255,0.3)] transition-all duration-300 text-foreground placeholder-foreground-muted/50"
|
className="w-full px-4 py-3 bg-input border border-input-border rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-transparent text-foreground"
|
||||||
placeholder="Your company name"
|
placeholder="Your company name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -215,7 +206,7 @@ const Contact = () => {
|
||||||
name="phone"
|
name="phone"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-3 bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-neon focus:bg-white/10 focus:shadow-[0_0_15px_rgba(51,102,255,0.3)] transition-all duration-300 text-foreground placeholder-foreground-muted/50"
|
className="w-full px-4 py-3 bg-input border border-input-border rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-transparent text-foreground"
|
||||||
placeholder="(361) 555-0123"
|
placeholder="(361) 555-0123"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,7 +223,7 @@ const Contact = () => {
|
||||||
rows={5}
|
rows={5}
|
||||||
value={formData.message}
|
value={formData.message}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-3 bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-neon focus:bg-white/10 focus:shadow-[0_0_15px_rgba(51,102,255,0.3)] transition-all duration-300 text-foreground placeholder-foreground-muted/50 resize-none"
|
className="w-full px-4 py-3 bg-input border border-input-border rounded-lg focus:outline-none focus:ring-2 focus:ring-neon focus:border-transparent text-foreground resize-none"
|
||||||
placeholder="Tell us about your IT needs or challenges..."
|
placeholder="Tell us about your IT needs or challenges..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -381,47 +372,6 @@ const Contact = () => {
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Service Area Map */}
|
|
||||||
<section className="py-16 bg-background-deep">
|
|
||||||
<div className="w-full max-w-[95%] mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<ScrollReveal width="100%">
|
|
||||||
<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>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const DesktopHardware = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-gray-900 via-gray-800 to-gray-600 text-white py-20">
|
<section className="bg-gradient-to-br from-gray-900 via-gray-800 to-gray-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -6,48 +6,18 @@ import ProcessTimeline from '@/components/home/ProcessTimeline';
|
||||||
import ServicesOverview from '@/components/home/ServicesOverview';
|
import ServicesOverview from '@/components/home/ServicesOverview';
|
||||||
import ProofSection from '@/components/home/ProofSection';
|
import ProofSection from '@/components/home/ProofSection';
|
||||||
import CTASection from '@/components/home/CTASection';
|
import CTASection from '@/components/home/CTASection';
|
||||||
import TrustedBy from '@/components/home/TrustedBy';
|
|
||||||
import ScrollReveal from '@/components/ScrollReveal';
|
|
||||||
import { useSEO } from '@/hooks/useSEO';
|
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
useSEO({
|
|
||||||
title: 'IT Support Corpus Christi ⚡ 24/7 Managed Services | Bay Area Affiliates',
|
|
||||||
description: 'Stop IT headaches! 25+ years experience, 2-hour response time, 24/7 monitoring. Managed IT services in Corpus Christi & Coastal Bend. Free assessment. Call (361) 765-8400',
|
|
||||||
keywords: 'managed IT services corpus christi, IT support coastal bend, 24/7 IT monitoring, business computer solutions, Windows 11 migration, VPN setup, network security corpus christi',
|
|
||||||
canonical: 'https://bayarea-cc.com/',
|
|
||||||
ogImage: 'https://bayarea-cc.com/og-image.png',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main id="main-content" role="main">
|
<main>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
|
||||||
<ScrollReveal delay={0.2} width="100%">
|
|
||||||
<TrustedBy />
|
|
||||||
</ScrollReveal>
|
|
||||||
|
|
||||||
<ScrollReveal width="100%">
|
|
||||||
<ValuePillars />
|
<ValuePillars />
|
||||||
</ScrollReveal>
|
|
||||||
|
|
||||||
<ScrollReveal width="100%">
|
|
||||||
<ProcessTimeline />
|
<ProcessTimeline />
|
||||||
</ScrollReveal>
|
|
||||||
|
|
||||||
<ScrollReveal width="100%">
|
|
||||||
<ServicesOverview />
|
<ServicesOverview />
|
||||||
</ScrollReveal>
|
|
||||||
|
|
||||||
<ScrollReveal width="100%">
|
|
||||||
<ProofSection />
|
<ProofSection />
|
||||||
</ScrollReveal>
|
|
||||||
|
|
||||||
<ScrollReveal width="100%">
|
|
||||||
<CTASection />
|
<CTASection />
|
||||||
</ScrollReveal>
|
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const NetworkAttachedStorage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-600 text-white py-20">
|
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const NetworkInfrastructure = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-teal-900 via-teal-800 to-teal-600 text-white py-20">
|
<section className="bg-gradient-to-br from-teal-900 via-teal-800 to-teal-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const PerformanceUpgrades = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-purple-900 via-purple-800 to-purple-600 text-white py-20">
|
<section className="bg-gradient-to-br from-purple-900 via-purple-800 to-purple-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const PrinterScannerInstallation = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-indigo-900 via-indigo-800 to-indigo-600 text-white py-20">
|
<section className="bg-gradient-to-br from-indigo-900 via-indigo-800 to-indigo-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,11 @@ import Footer from '@/components/Footer';
|
||||||
import { Monitor, Wifi, Cloud, Shield, Database, Settings, CheckCircle } from 'lucide-react';
|
import { Monitor, Wifi, Cloud, Shield, Database, Settings, CheckCircle } from 'lucide-react';
|
||||||
import ScrollReveal from '@/components/ScrollReveal';
|
import ScrollReveal from '@/components/ScrollReveal';
|
||||||
import { useLayoutEffect, useRef } from 'react';
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
import { useSEO } from '@/hooks/useSEO';
|
|
||||||
|
|
||||||
const Services = () => {
|
const Services = () => {
|
||||||
const pageRef = useRef<HTMLDivElement>(null);
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
const heroImageRef = useRef<HTMLImageElement>(null);
|
const heroImageRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
useSEO({
|
|
||||||
title: 'IT Services Corpus Christi | Managed IT, Network, Cloud | Bay Area Affiliates',
|
|
||||||
description: 'Complete IT solutions for Corpus Christi businesses. Hardware support, network infrastructure, cloud services, secure remote access, backup & Microsoft 365. Free consultation.',
|
|
||||||
keywords: 'IT services Corpus Christi, managed IT Coastal Bend, network infrastructure Texas, cloud services, VPN setup, Microsoft 365',
|
|
||||||
canonical: 'https://bayarea-cc.com/services',
|
|
||||||
ogImage: 'https://bayarea-cc.com/og-image.png',
|
|
||||||
});
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Dynamically import GSAP only when needed to reduce initial bundle size
|
// Dynamically import GSAP only when needed to reduce initial bundle size
|
||||||
let ctx: any;
|
let ctx: any;
|
||||||
|
|
@ -172,7 +163,7 @@ const Services = () => {
|
||||||
<div ref={pageRef} className="min-h-screen">
|
<div ref={pageRef} className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero section with image background */}
|
{/* Hero section with image background */}
|
||||||
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
<section className="relative h-screen flex items-center justify-center overflow-hidden">
|
||||||
{/* Background image with parallax */}
|
{/* Background image with parallax */}
|
||||||
|
|
@ -183,7 +174,7 @@ const Services = () => {
|
||||||
alt="Corpus Christi IT services data center with enterprise networking equipment and server infrastructure"
|
alt="Corpus Christi IT services data center with enterprise networking equipment and server infrastructure"
|
||||||
className="w-full h-[110%] object-cover will-change-transform"
|
className="w-full h-[110%] object-cover will-change-transform"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
fetchPriority="high"
|
fetchpriority="high"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const VpnSetup = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-green-900 via-green-800 to-green-600 text-white py-20">
|
<section className="bg-gradient-to-br from-green-900 via-green-800 to-green-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const WebServices = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-emerald-900 via-emerald-800 to-emerald-600 text-white py-20">
|
<section className="bg-gradient-to-br from-emerald-900 via-emerald-800 to-emerald-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const Windows11Transition = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main role="main">
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-600 text-white py-20">
|
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-600 text-white py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||