Changes für lighthouse Branch

This commit is contained in:
Timo Knuth 2026-01-09 09:17:39 +01:00
parent 9938c1f9e2
commit 1c5f272f33
40 changed files with 2636 additions and 370 deletions

View File

@ -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>Managed IT Services Corpus Christi | Bay Area Affiliates</title> <title>IT Support Corpus Christi ⚡ 24/7 Managed Services | Bay Area Affiliates</title>
<meta name="description" content="Secure, tailored IT support—Corpus Christi's trusted experts for 25+ years. Call today for a free assessment." /> <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="keywords" content="managed IT services corpus christi, IT support coastal bend, business computer solutions, Portland IT services, computer repair corpus christi" /> <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="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,10 +211,22 @@
} }
</script> </script>
<!-- Performance Optimization -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
<link rel="preload" href="/logo_bayarea.svg" as="image" />
<link rel="preload" href="/serverroom.webp" as="image" type="image/webp" />
<!-- 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="icon" type="image/png" href="/logo_bayarea.svg" /> <link rel="apple-touch-icon" sizes="180x180" 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>

682
package-lock.json generated
View File

@ -42,6 +42,7 @@
"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",
@ -73,10 +74,13 @@
"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": {
@ -150,6 +154,17 @@
"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",
@ -859,6 +874,496 @@
"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",
@ -908,6 +1413,17 @@
"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",
@ -3511,6 +4027,13 @@
"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",
@ -3868,6 +4391,16 @@
"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",
@ -4362,6 +4895,33 @@
"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",
@ -4874,6 +5434,21 @@
"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",
@ -5683,9 +6258,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -5695,6 +6270,51 @@
"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",
@ -5738,6 +6358,16 @@
"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",
@ -5747,6 +6377,17 @@
"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",
@ -5959,6 +6600,32 @@
"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",
@ -6705,6 +7372,13 @@
"@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",

View File

@ -45,6 +45,7 @@
"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",
@ -76,9 +77,12 @@
"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"
} }
} }

33
public/_headers Normal file
View File

@ -0,0 +1,33 @@
# 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

BIN
public/about_banner.avif Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 324 KiB

BIN
public/about_banner.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
public/blog_banner.avif Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 272 KiB

BIN
public/blog_banner.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/contact_banner.avif Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 144 KiB

BIN
public/contact_banner.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
public/faster_happier.avif Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 469 KiB

BIN
public/faster_happier.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

22
public/manifest.json Normal file
View File

@ -0,0 +1,22 @@
{
"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"
}

BIN
public/serverroom.avif Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 217 KiB

BIN
public/serverroom.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

113
scripts/optimize-images.cjs Normal file
View File

@ -0,0 +1,113 @@
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));

View File

@ -2,8 +2,12 @@ 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 } from "react-router-dom"; import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import { lazy, Suspense } from "react"; import { lazy, Suspense, useEffect } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { reportWebVitals, observePerformance } from "@/utils/reportWebVitals";
import { pageTransition } from "@/utils/animations";
import ExitIntentPopup from "@/components/ExitIntentPopup";
// Eager load critical pages for better initial performance // Eager load critical pages for better initial performance
import Index from "./pages/Index"; import Index from "./pages/Index";
@ -26,42 +30,101 @@ const NetworkAttachedStorage = lazy(() => import("./pages/NetworkAttachedStorage
const queryClient = new QueryClient(); const queryClient = new QueryClient();
// Loading fallback component // Loading fallback component with animation
const PageLoader = () => ( const PageLoader = () => (
<div className="min-h-screen flex items-center justify-center bg-background-deep"> <motion.div
className="min-h-screen flex items-center justify-center bg-background-deep"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div className="text-center"> <div className="text-center">
<div className="w-16 h-16 border-4 border-neon/30 border-t-neon rounded-full animate-spin mx-auto mb-4"></div> <motion.div
<p className="text-foreground-muted">Loading...</p> className="w-16 h-16 border-4 border-neon/30 border-t-neon rounded-full mx-auto mb-4"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
/>
<motion.p
className="text-foreground-muted"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
Loading...
</motion.p>
</div> </div>
</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>
<Suspense fallback={<PageLoader />}> <AppContent />
<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>

View File

@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { X, Download } from 'lucide-react';
import { Link } from 'react-router-dom';
const ExitIntentPopup = () => {
const [isVisible, setIsVisible] = useState(false);
const [hasShown, setHasShown] = useState(false);
useEffect(() => {
// Check if popup was already shown in this session
const shown = sessionStorage.getItem('exitIntentShown');
if (shown) {
setHasShown(true);
return;
}
const handleMouseLeave = (e: MouseEvent) => {
// Only trigger if mouse is leaving from top of viewport
if (e.clientY <= 0 && !hasShown) {
setIsVisible(true);
setHasShown(true);
sessionStorage.setItem('exitIntentShown', 'true');
}
};
// Add delay before activating to avoid false triggers
const timer = setTimeout(() => {
document.addEventListener('mouseleave', handleMouseLeave);
}, 3000);
return () => {
clearTimeout(timer);
document.removeEventListener('mouseleave', handleMouseLeave);
};
}, [hasShown]);
const handleClose = () => {
setIsVisible(false);
};
if (!isVisible) return null;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 animate-in fade-in duration-300"
onClick={handleClose}
/>
{/* Popup */}
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 animate-in zoom-in duration-300">
<div className="card-dark p-8 max-w-lg w-[90vw] relative">
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 text-foreground-muted hover:text-foreground transition-colors"
aria-label="Close popup"
>
<X className="w-6 h-6" />
</button>
{/* Content */}
<div className="text-center">
<div className="w-16 h-16 bg-neon/20 rounded-full flex items-center justify-center mx-auto mb-4">
<Download className="w-8 h-8 text-neon" />
</div>
<h2 className="font-heading font-bold text-2xl sm:text-3xl text-foreground mb-4">
Wait! Don't leave empty-handed
</h2>
<p className="text-foreground-muted mb-6 text-lg">
Download our <span className="text-neon font-semibold">free Windows 11 Migration Checklist</span>
a $299 value guide to help you prepare for the upgrade.
</p>
{/* Key benefits */}
<ul className="text-left space-y-2 mb-6 text-sm">
<li className="flex items-start gap-2">
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Hardware compatibility check (avoid costly mistakes)</span>
</li>
<li className="flex items-start gap-2">
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Step-by-step migration timeline</span>
</li>
<li className="flex items-start gap-2">
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Security & compliance considerations</span>
</li>
<li className="flex items-start gap-2">
<svg className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Rollback plan (just in case)</span>
</li>
</ul>
{/* CTA */}
<Link
to="/contact?lead=windows11-checklist"
onClick={handleClose}
className="btn-neon w-full flex items-center justify-center space-x-2 mb-3"
>
<Download className="w-5 h-5" />
<span>Download Free Checklist</span>
</Link>
<p className="text-xs text-foreground-muted">
No spam. No credit card required. Instant access.
</p>
{/* Close link */}
<button
onClick={handleClose}
className="text-sm text-foreground-muted hover:text-foreground mt-4 underline"
>
No thanks, I'll figure it out myself
</button>
</div>
</div>
</div>
</>
);
};
export default ExitIntentPopup;

View File

@ -1,6 +1,8 @@
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);
@ -27,87 +29,173 @@ 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 <>
? 'bg-white/5 backdrop-blur-2xl border-b border-white/20 shadow-2xl shadow-black/20' {/* Skip link for accessibility */}
: 'bg-transparent' <a href="#main-content" className="skip-link">
}`}> Skip to main content
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> </a>
<div className="flex items-center justify-between h-16">
{/* Logo */} <motion.nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
? 'bg-white/5 backdrop-blur-2xl border-b border-white/20 shadow-2xl shadow-black/20'
: 'bg-transparent'
}`}
initial="hidden"
animate="visible"
variants={navVariants}
role="navigation"
aria-label="Main navigation"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14 md:h-16">
{/* Logo with animation */}
<Link to="/" className="flex items-center space-x-3"> <Link to="/" className="flex items-center space-x-3">
<div className="w-12 h-12 flex items-center justify-center overflow-visible"> <motion.div
className="w-12 h-12 flex items-center justify-center overflow-visible"
whileHover={{ rotate: 360 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
>
<img <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 hover:opacity-100 transition-opacity duration-300" className="w-10 h-10 opacity-90"
/> />
</div> </motion.div>
<span className="font-heading font-bold text-lg text-white"> <span className="font-heading font-bold text-lg text-white">
Bay Area Affiliates Bay Area Affiliates
</span> </span>
</Link> </Link>
{/* Desktop Navigation */} {/* Desktop Navigation with animations */}
<div className="hidden md:flex items-center space-x-8"> <div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => ( {navItems.map((item, index) => (
<Link <motion.div
key={item.name} key={item.name}
to={item.path} initial={{ opacity: 0, y: -20 }}
className={`font-medium transition-colors duration-200 ${isActive(item.path) animate={{ opacity: 1, y: 0 }}
? 'text-neon' transition={{ delay: index * 0.1, duration: 0.5 }}
: 'text-white hover:text-neon'
}`}
> >
{item.name}
</Link>
))}
<Link
to="/contact"
className="btn-neon ml-4"
>
Get Started
</Link>
</div>
{/* Mobile menu button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden text-white hover:text-neon transition-colors"
aria-label="Toggle navigation menu"
>
{isOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile Navigation */}
{isOpen && (
<div className="md:hidden bg-white/5 backdrop-blur-2xl border-t border-white/20">
<div className="px-2 pt-2 pb-3 space-y-1">
{navItems.map((item) => (
<Link <Link
key={item.name}
to={item.path} to={item.path}
onClick={() => setIsOpen(false)} className={`font-medium transition-all duration-200 relative group px-2 py-1 ${isActive(item.path)
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${isActive(item.path) ? 'text-neon'
? 'text-neon bg-neon/10' : 'text-white hover:text-neon'
: 'text-white hover:text-neon hover:bg-neon/5'
}`} }`}
> >
{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"
onClick={() => setIsOpen(false)} className="btn-neon ml-4"
className="block w-full text-center btn-neon mt-4"
> >
Get Started Get Started
</Link> </Link>
</div> </motion.div>
</div> </div>
)}
{/* Mobile menu button with animation */}
<motion.button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden text-white hover:text-neon transition-colors"
aria-label="Toggle navigation menu"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<AnimatePresence mode="wait">
{isOpen ? (
<motion.div
key="close"
initial={{ rotate: -90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: 90, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<X size={24} />
</motion.div>
) : (
<motion.div
key="menu"
initial={{ rotate: 90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: -90, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Menu size={24} />
</motion.div>
)}
</AnimatePresence>
</motion.button>
</div>
{/* Mobile Navigation with smooth animations */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="md:hidden bg-white/5 backdrop-blur-2xl border-t border-white/20 overflow-hidden"
>
<motion.div
className="px-2 pt-2 pb-3 space-y-1"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
{navItems.map((item) => (
<motion.div key={item.name} variants={staggerItem}>
<Link
to={item.path}
onClick={() => setIsOpen(false)}
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${isActive(item.path)
? 'text-neon bg-neon/10'
: 'text-white hover:text-neon hover:bg-neon/5'
}`}
>
{item.name}
</Link>
</motion.div>
))}
<motion.div variants={staggerItem}>
<Link
to="/contact"
onClick={() => setIsOpen(false)}
className="block w-full text-center btn-neon mt-4"
>
Get Started
</Link>
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div> </div>
</nav> </motion.nav>
</>
); );
}; };

View File

@ -1,8 +1,7 @@
import { ReactNode, useLayoutEffect, useRef } from 'react'; import { ReactNode } from 'react';
import gsap from 'gsap'; import { motion, useInView } from 'framer-motion';
import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { useRef } from 'react';
import { scrollRevealVariants } from '@/utils/animations';
gsap.registerPlugin(ScrollTrigger);
type ScrollRevealProps = { type ScrollRevealProps = {
children: ReactNode; children: ReactNode;
@ -11,38 +10,20 @@ type ScrollRevealProps = {
}; };
const ScrollReveal = ({ children, delay = 0, className = '' }: ScrollRevealProps) => { const ScrollReveal = ({ children, delay = 0, className = '' }: ScrollRevealProps) => {
const elementRef = useRef<HTMLDivElement>(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
useLayoutEffect(() => {
const element = elementRef.current;
if (!element) return;
const ctx = gsap.context(() => {
gsap.fromTo(
element,
{ autoAlpha: 0, y: 32 },
{
autoAlpha: 1,
y: 0,
duration: 0.8,
ease: 'power3.out',
delay: delay / 1000,
scrollTrigger: {
trigger: element,
start: 'top 85%',
once: true,
},
}
);
}, element);
return () => ctx.revert();
}, [delay]);
return ( return (
<div ref={elementRef} className={className}> <motion.div
ref={ref}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
variants={scrollRevealVariants}
transition={{ delay: delay / 1000 }}
className={className}
>
{children} {children}
</div> </motion.div>
); );
}; };

View File

@ -0,0 +1,200 @@
import { motion } from 'framer-motion';
import { useMemo } from 'react';
const BackgroundAnimations = () => {
// Reduced particles for better performance (30 -> 12)
const particles = useMemo(() => {
return Array.from({ length: 12 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 2 + 1,
duration: Math.random() * 12 + 8,
delay: Math.random() * 4,
opacity: Math.random() * 0.5 + 0.2,
}));
}, []);
// Reduced circuit nodes for performance (12 -> 6)
const circuitNodes = useMemo(() => {
return Array.from({ length: 6 }, (_, i) => ({
id: i,
x: Math.random() * 80 + 10,
y: Math.random() * 80 + 10,
pulseDelay: Math.random() * 2,
}));
}, []);
// Reduced connection lines (half as many)
const connectionLines = useMemo(() => {
const lines = [];
for (let i = 0; i < circuitNodes.length - 1; i += 2) {
if (i + 1 < circuitNodes.length) {
lines.push({
id: i,
x1: circuitNodes[i].x,
y1: circuitNodes[i].y,
x2: circuitNodes[i + 1].x,
y2: circuitNodes[i + 1].y,
});
}
}
return lines;
}, [circuitNodes]);
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none" style={{ willChange: 'transform' }}>
{/* Simplified Static Grid Background - no animation for better performance */}
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage: `
linear-gradient(to right, rgba(51, 102, 255, 0.03) 1px, transparent 1px),
linear-gradient(to bottom, rgba(51, 102, 255, 0.03) 1px, transparent 1px)
`,
backgroundSize: '80px 80px',
willChange: 'transform',
}}
/>
{/* SVG Container for Lines and Nodes - optimized */}
<svg className="absolute inset-0 w-full h-full" style={{ opacity: 0.5, willChange: 'transform' }}>
{/* Simplified Connection Lines - static for better performance */}
{connectionLines.map((line) => (
<line
key={line.id}
x1={`${line.x1}%`}
y1={`${line.y1}%`}
x2={`${line.x2}%`}
y2={`${line.y2}%`}
stroke="rgba(51, 102, 255, 0.2)"
strokeWidth="1"
opacity="0.4"
/>
))}
{/* Reduced Vertical Data Streams (8 -> 4) */}
{[...Array(4)].map((_, i) => (
<motion.line
key={`vertical-${i}`}
x1={`${15 + i * 25}%`}
y1="0%"
x2={`${15 + i * 25}%`}
y2="100%"
stroke="rgba(51, 102, 255, 0.15)"
strokeWidth="1"
strokeDasharray="10 20"
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: 100 }}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear",
delay: i * 0.5,
}}
/>
))}
{/* Simplified Circuit Nodes - reduced animation */}
{circuitNodes.map((node) => (
<g key={node.id}>
{/* Core node with simple pulse */}
<motion.circle
cx={`${node.x}%`}
cy={`${node.y}%`}
r="2.5"
fill="#3366ff"
animate={{
opacity: [0.6, 1, 0.6],
}}
transition={{
duration: 3,
repeat: Infinity,
delay: node.pulseDelay,
ease: "easeInOut",
}}
/>
</g>
))}
</svg>
{/* Optimized Floating Data Particles with GPU acceleration */}
{particles.map((particle) => (
<motion.div
key={particle.id}
className="absolute rounded-full bg-neon"
style={{
width: `${particle.size}px`,
height: `${particle.size}px`,
left: `${particle.x}%`,
top: `${particle.y}%`,
opacity: particle.opacity,
boxShadow: '0 0 6px rgba(51, 102, 255, 0.6)',
willChange: 'transform, opacity',
}}
animate={{
y: [0, -600],
opacity: [0, particle.opacity, 0],
}}
transition={{
duration: particle.duration,
repeat: Infinity,
delay: particle.delay,
ease: "linear",
}}
/>
))}
{/* Simplified Static Scanline Effect - no animation */}
<div
className="absolute inset-0 opacity-20 pointer-events-none"
style={{
background: `repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(51, 102, 255, 0.02) 2px,
rgba(51, 102, 255, 0.02) 4px
)`,
}}
/>
{/* Reduced Radial Glows (3 -> 2) with simpler animation */}
{[
{ x: 25, y: 40 },
{ x: 75, y: 60 },
].map((pos, i) => (
<motion.div
key={`glow-${i}`}
className="absolute rounded-full pointer-events-none"
style={{
left: `${pos.x}%`,
top: `${pos.y}%`,
width: '180px',
height: '180px',
background: 'radial-gradient(circle, rgba(51, 102, 255, 0.12) 0%, transparent 70%)',
transform: 'translate(-50%, -50%)',
willChange: 'transform',
}}
animate={{
scale: [1, 1.15, 1],
opacity: [0.4, 0.6, 0.4],
}}
transition={{
duration: 6,
repeat: Infinity,
delay: i * 2,
ease: "easeInOut",
}}
/>
))}
{/* Static Corner Accent Glows - no blur for better performance */}
<div className="absolute top-0 left-0 w-64 h-64 bg-gradient-to-br from-neon/8 to-transparent rounded-full opacity-40 pointer-events-none" />
<div className="absolute bottom-0 right-0 w-80 h-80 bg-gradient-to-tl from-neon/8 to-transparent rounded-full opacity-40 pointer-events-none" />
</div>
);
};
export default BackgroundAnimations;

View File

@ -1,6 +1,8 @@
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 ScrollReveal from '../ScrollReveal'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { fadeInUp, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations';
const CTASection = () => { const CTASection = () => {
const faqs = [ const faqs = [
@ -21,6 +23,13 @@ const CTASection = () => {
} }
]; ];
const headerRef = useRef(null);
const ctaRef = useRef(null);
const faqRef = useRef(null);
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
const isCtaInView = useInView(ctaRef, { once: true, margin: "-100px" });
const isFaqInView = useInView(faqRef, { once: true, margin: "-100px" });
return ( 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 */}
@ -29,88 +38,141 @@ 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">
<ScrollReveal> <motion.div
ref={headerRef}
initial="hidden"
animate={isHeaderInView ? "visible" : "hidden"}
variants={fadeInUp}
>
<h2 className="font-heading font-bold text-4xl sm:text-5xl lg:text-6xl text-foreground mb-6"> <h2 className="font-heading font-bold text-4xl sm:text-5xl lg:text-6xl text-foreground mb-6">
Ready for <span className="text-neon text-glow">reliable IT?</span> Ready for <motion.span
className="text-neon text-glow"
animate={{
textShadow: [
"0 0 20px rgba(51, 102, 255, 0.5)",
"0 0 40px rgba(51, 102, 255, 0.8)",
"0 0 20px rgba(51, 102, 255, 0.5)"
]
}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
>
reliable IT?
</motion.span>
</h2> </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>
</ScrollReveal> </motion.div>
{/* Primary CTAs */} {/* Primary CTAs */}
<ScrollReveal delay={200}> <motion.div
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16"> ref={ctaRef}
initial={{ opacity: 0, y: 30 }}
animate={isCtaInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
transition={{ delay: 0.2, duration: 0.6 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16"
>
<motion.div whileHover={buttonHover} whileTap={buttonTap}>
<Link <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>
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" /> <motion.div
animate={{ x: [0, 3, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<ArrowRight className="w-5 h-5" />
</motion.div>
</Link> </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>
</div> </motion.div>
</ScrollReveal> </motion.div>
{/* Micro FAQ */} {/* Micro FAQ */}
<ScrollReveal delay={400}> <motion.div
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> ref={faqRef}
{faqs.map((faq, index) => { initial="hidden"
const Icon = faq.icon; animate={isFaqInView ? "visible" : "hidden"}
variants={staggerContainer}
return ( className="grid grid-cols-1 md:grid-cols-3 gap-8"
<div key={faq.question} className="text-left"> >
<div className="flex items-start space-x-3"> {faqs.map((faq, index) => {
<div className="w-8 h-8 bg-neon/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"> const Icon = faq.icon;
<Icon className="w-4 h-4 text-neon" />
</div> return (
<div> <motion.div
<h3 className="font-semibold text-foreground mb-2"> key={faq.question}
{faq.question} variants={staggerItem}
</h3> className="text-left"
<p className="text-sm text-foreground-muted"> >
{faq.answer} <motion.div
</p> className="flex items-start space-x-3"
</div> whileHover={{ x: 5 }}
transition={{ duration: 0.2 }}
>
<motion.div
className="w-8 h-8 bg-neon/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"
whileHover={{ rotate: 360, scale: 1.1 }}
transition={{ duration: 0.5 }}
>
<Icon className="w-4 h-4 text-neon" />
</motion.div>
<div>
<h3 className="font-semibold text-foreground mb-2">
{faq.question}
</h3>
<p className="text-sm text-foreground-muted">
{faq.answer}
</p>
</div> </div>
</div> </motion.div>
); </motion.div>
})} );
</div> })}
</ScrollReveal> </motion.div>
{/* Contact info */} {/* Contact info */}
<ScrollReveal delay={600}> <motion.div
<div className="mt-16 pt-8 border-t border-border/30"> initial={{ opacity: 0, y: 20 }}
<p className="text-sm text-foreground-muted mb-4"> animate={isFaqInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
Ready to talk? We're here to help. transition={{ delay: 0.6, duration: 0.6 }}
</p> className="mt-16 pt-8 border-t border-border/30"
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center text-sm"> >
<a <p className="text-sm text-foreground-muted mb-4">
href="tel:+1-361-555-0123" Ready to talk? We're here to help.
className="text-neon hover:text-neon/80 transition-colors flex items-center" </p>
> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center text-sm">
<Phone className="w-4 h-4 mr-2" /> <motion.a
(361) 555-0123 href="tel:+1-361-555-0123"
</a> className="text-neon hover:text-neon/80 transition-colors flex items-center"
<span className="hidden sm:block text-border">|</span> whileHover={{ scale: 1.05 }}
<a whileTap={{ scale: 0.95 }}
href="mailto:info@bayareaaffiliates.com" >
className="text-neon hover:text-neon/80 transition-colors" <Phone className="w-4 h-4 mr-2" />
> (361) 555-0123
info@bayareaaffiliates.com </motion.a>
</a> <span className="hidden sm:block text-border">|</span>
</div> <motion.a
href="mailto:info@bayareaaffiliates.com"
className="text-neon hover:text-neon/80 transition-colors"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
info@bayareaaffiliates.com
</motion.a>
</div> </div>
</ScrollReveal> </motion.div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -1,87 +1,165 @@
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 { useEffect, useRef } from 'react'; import { motion, useScroll, useTransform } from 'framer-motion';
import heroNetwork from '@/assets/hero-network.jpg'; import { heroVariants, heroItemVariants, buttonHover, buttonTap, easing } from '@/utils/animations';
import BackgroundAnimations from './BackgroundAnimations';
const HeroSection = () => { const HeroSection = () => {
const imageRef = useRef<HTMLImageElement>(null); const { scrollY } = useScroll();
useEffect(() => { // Smooth parallax effect with Framer Motion
const handleScroll = () => { const y = useTransform(scrollY, [0, 500], [0, 150]);
if (imageRef.current) { const opacity = useTransform(scrollY, [0, 300], [1, 0]);
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 parallax */} {/* Background image with smooth parallax */}
<div className="absolute inset-0 overflow-hidden"> <motion.div
<img className="absolute inset-0 overflow-hidden"
ref={imageRef} style={{ y }}
src="/serverroom.png" >
alt="Modern IT infrastructure with network connections" <picture>
className="w-full h-[110%] object-cover will-change-transform" <source type="image/avif" srcSet="/serverroom.avif" />
style={{ transform: 'translateY(0px) scale(1.1)' }} <source type="image/webp" srcSet="/serverroom.webp" />
/> <motion.img
{/* Dark overlay */} src="/serverroom.png"
<div className="absolute inset-0 bg-black/35"></div> alt="Modern IT infrastructure with network connections and server equipment"
</div> className="w-full h-[110%] object-cover"
initial={{ scale: 1.1, opacity: 0 }}
animate={{ scale: 1.1, opacity: 1 }}
transition={{ duration: 1.2, ease: easing.elegant }}
loading="eager"
fetchpriority="high"
/>
</picture>
{/* Main content */} {/* Animated background effects */}
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <BackgroundAnimations />
{/* Darker overlay for better text contrast */}
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/50 to-black/60"></div>
</motion.div>
{/* Main content with staggered animations */}
<motion.div
className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"
variants={heroVariants}
initial="hidden"
animate="visible"
style={{ opacity }}
>
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
{/* Badge */} {/* Badge */}
<div className="inline-flex items-center px-4 py-2 rounded-full bg-neon/20 border border-neon/40 text-neon text-sm font-medium mb-8 drop-shadow-[0_0_10px_rgba(51,102,255,0.5)]"> <motion.div
<span className="w-2 h-2 bg-neon rounded-full mr-2 animate-glow-pulse"></span> variants={heroItemVariants}
className="inline-flex items-center px-4 py-2 rounded-full bg-neon/20 border border-neon/40 text-neon text-sm font-medium mb-8 drop-shadow-[0_0_10px_rgba(51,102,255,0.5)]"
>
<motion.span
className="w-2 h-2 bg-neon rounded-full mr-2"
animate={{
scale: [1, 1.2, 1],
opacity: [1, 0.7, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
Serving the Coastal Bend since 2010 Serving the Coastal Bend since 2010
</div> </motion.div>
{/* Main headline */} {/* Main headline */}
<h1 className="font-heading font-bold text-5xl sm:text-6xl lg:text-7xl text-white mb-6 text-balance drop-shadow-[0_0_20px_rgba(255,255,255,0.3)]"> <motion.h1
variants={heroItemVariants}
className="font-heading font-bold text-5xl sm:text-6xl lg:text-7xl text-white mb-6 text-balance drop-shadow-[0_0_20px_rgba(255,255,255,0.3)]"
>
Modern IT that keeps your{' '} Modern IT that keeps your{' '}
<span className="text-neon text-glow drop-shadow-[0_0_30px_rgba(51,102,255,0.8)]">business moving</span> <motion.span
</h1> className="text-neon text-glow drop-shadow-[0_0_30px_rgba(51,102,255,0.8)]"
animate={{
textShadow: [
'0 0 20px rgba(51,102,255,0.5)',
'0 0 30px rgba(51,102,255,0.8)',
'0 0 20px rgba(51,102,255,0.5)',
],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
>
business moving
</motion.span>
</motion.h1>
{/* Subheadline */} {/* Subheadline */}
<p className="text-xl sm:text-2xl text-white/95 mb-12 max-w-3xl mx-auto leading-relaxed drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]"> <motion.p
variants={heroItemVariants}
className="text-xl sm:text-2xl text-white/95 mb-12 max-w-3xl mx-auto leading-relaxed drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]"
>
From fast devices to secure remote access and resilient networks we design, 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.
</p> </motion.p>
{/* CTA buttons */} {/* CTA buttons with hover animations */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <motion.div
<Link variants={heroItemVariants}
to="/contact" className="flex flex-col sm:flex-row gap-4 justify-center items-center"
className="btn-neon group flex items-center space-x-2" >
<motion.div
whileHover={buttonHover}
whileTap={buttonTap}
> >
<span>Get in touch</span> <Link
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" /> to="/contact"
</Link> className="btn-neon group flex items-center space-x-2"
>
<span>Get in touch</span>
<motion.div
animate={{ x: [0, 3, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<ArrowRight className="w-5 h-5" />
</motion.div>
</Link>
</motion.div>
<button className="btn-ghost group flex items-center space-x-2"> <motion.div
<Play className="w-5 h-5 transition-transform group-hover:scale-110" /> whileHover={buttonHover}
<span>See our system</span> whileTap={buttonTap}
</button> >
</div> <Link
to="/services"
className="btn-ghost group flex items-center space-x-2"
>
<Play className="w-5 h-5" />
<span>See our services</span>
</Link>
</motion.div>
</motion.div>
</div> </div>
</div> </motion.div>
{/* Scroll indicator */} {/* Animated scroll indicator */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2"> <motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.5, duration: 0.8 }}
>
<div className="w-6 h-10 border-2 border-neon/60 rounded-full flex justify-center drop-shadow-[0_0_10px_rgba(51,102,255,0.3)]"> <div className="w-6 h-10 border-2 border-neon/60 rounded-full flex justify-center drop-shadow-[0_0_10px_rgba(51,102,255,0.3)]">
<div className="w-1 h-3 bg-neon rounded-full mt-2 animate-bounce"></div> <motion.div
className="w-1 h-3 bg-neon rounded-full mt-2"
animate={{ y: [0, 12, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
/>
</div> </div>
</div> </motion.div>
</div> </div>
</section> </section>
); );

View File

@ -1,10 +1,20 @@
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 ScrollReveal from '../ScrollReveal'; import { motion, useInView, useScroll, useTransform } from 'framer-motion';
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 = [
{ {
@ -63,7 +73,12 @@ 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">
<ScrollReveal> <motion.div
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
@ -72,14 +87,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>
</ScrollReveal> </motion.div>
<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">
<div <motion.div
className="absolute top-0 left-0 w-full bg-neon transition-all duration-500 ease-out" className="absolute top-0 left-0 w-full bg-neon"
style={{ height: `${(activeStep + 1) * 25}%` }} style={{ height: useTransform(timelineProgress, (value) => `${value}%`) }}
/> />
</div> </div>
@ -89,17 +104,35 @@ 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 (
<ScrollReveal key={step.title} delay={index * 100}> <motion.div
key={step.title}
ref={stepRef}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
variants={fadeInUp}
transition={{ delay: index * 0.1 }}
>
<div className={`relative flex flex-col lg:flex-row items-center ${ <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 */}
<div className={`flex-1 ${isEven ? 'lg:pr-16' : 'lg:pl-16'} ${ <motion.div
isEven ? 'lg:text-right' : 'lg:text-left' className={`flex-1 ${isEven ? 'lg:pr-16' : 'lg:pl-16'} ${
} text-center lg:text-left`}> isEven ? 'lg:text-right' : 'lg:text-left'
<div className="card-dark p-8 max-w-lg mx-auto lg:mx-0"> } text-center lg:text-left`}
initial={{ opacity: 0, x: isEven ? -40 : 40 }}
animate={isInView ? { opacity: 1, x: 0 } : { opacity: 0, x: isEven ? -40 : 40 }}
transition={{ delay: index * 0.1 + 0.2, duration: 0.6 }}
>
<motion.div
className="card-dark p-8 max-w-lg mx-auto lg:mx-0"
whileHover={{ y: -5, boxShadow: "0 0 30px rgba(51, 102, 255, 0.3)" }}
transition={{ duration: 0.3 }}
>
<div className="mb-4"> <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}
@ -114,24 +147,34 @@ const ProcessTimeline = () => {
<p className="text-sm text-foreground-muted"> <p className="text-sm text-foreground-muted">
{step.details} {step.details}
</p> </p>
</div> </motion.div>
</div> </motion.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">
<div className={`w-16 h-16 rounded-full border-4 flex items-center justify-center transition-all duration-500 ${ <motion.div
isActive className={`w-16 h-16 rounded-full border-4 flex items-center justify-center ${
? 'border-neon bg-neon text-neon-foreground shadow-neon' isActive
: 'border-border bg-background text-foreground-muted' ? 'border-neon bg-neon text-neon-foreground shadow-neon'
}`}> : 'border-border bg-background text-foreground-muted'
}`}
initial={{ scale: 0, rotate: -180 }}
animate={isInView ? { scale: 1, rotate: 0 } : { scale: 0, rotate: -180 }}
transition={{
delay: index * 0.1 + 0.3,
duration: 0.6,
type: "spring",
stiffness: 200
}}
>
<Icon className="w-6 h-6" /> <Icon className="w-6 h-6" />
</div> </motion.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>
</ScrollReveal> </motion.div>
); );
})} })}
</div> </div>

View File

@ -1,6 +1,8 @@
import { MapPin, Star, Users } from 'lucide-react'; import { MapPin, Star, Users } from 'lucide-react';
import ScrollReveal from '../ScrollReveal'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import CountUpNumber from '../CountUpNumber'; import CountUpNumber from '../CountUpNumber';
import { fadeInUp, scaleIn, staggerContainer, staggerItem } from '@/utils/animations';
const ProofSection = () => { const ProofSection = () => {
const stats = [ const stats = [
@ -17,6 +19,13 @@ 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 */}
@ -24,12 +33,27 @@ const ProofSection = () => {
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-96 h-96 bg-neon/5 rounded-full blur-3xl"></div> <div className="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">
<ScrollReveal> <motion.div
ref={headerRef}
initial="hidden"
animate={isHeaderInView ? "visible" : "hidden"}
variants={fadeInUp}
>
<div className="text-center mb-16"> <div className="text-center mb-16">
<div className="inline-flex items-center px-4 py-2 rounded-full bg-neon/10 border border-neon/30 text-neon text-sm font-medium mb-8"> <motion.div
className="inline-flex items-center px-4 py-2 rounded-full bg-neon/10 border border-neon/30 text-neon text-sm font-medium mb-8"
animate={{
boxShadow: [
"0 0 20px rgba(51, 102, 255, 0.2)",
"0 0 30px rgba(51, 102, 255, 0.4)",
"0 0 20px rgba(51, 102, 255, 0.2)"
]
}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
>
<MapPin className="w-4 h-4 mr-2" /> <MapPin className="w-4 h-4 mr-2" />
Proudly serving the Coastal Bend Proudly serving the Coastal Bend
</div> </motion.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{' '}
@ -42,48 +66,102 @@ const ProofSection = () => {
tailored solutions that work in the real world. tailored solutions that work in the real world.
</p> </p>
</div> </div>
</ScrollReveal> </motion.div>
{/* Stats */} {/* Stats */}
<ScrollReveal delay={200}> <motion.div
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-20"> ref={statsRef}
{stats.map((stat, index) => ( initial="hidden"
<div key={stat.label} className="text-center"> animate={isStatsInView ? "visible" : "hidden"}
<div className="font-heading font-bold text-4xl lg:text-5xl text-neon mb-2"> variants={staggerContainer}
<CountUpNumber className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-20"
value={stat.value} >
duration={2000 + index * 200} {stats.map((stat, index) => (
className="inline-block" <motion.div
/> key={stat.label}
</div> variants={staggerItem}
<div className="text-foreground-muted text-sm lg:text-base"> className="text-center"
{stat.label} >
</div> <motion.div
</div> className="font-heading font-bold text-4xl lg:text-5xl text-neon mb-2"
))} initial={{ scale: 0, rotate: -180 }}
</div> animate={isStatsInView ? { scale: 1, rotate: 0 } : { scale: 0, rotate: -180 }}
</ScrollReveal> transition={{
delay: index * 0.1,
duration: 0.6,
type: "spring",
stiffness: 150
}}
>
<CountUpNumber
value={stat.value}
duration={2000 + index * 200}
className="inline-block"
/>
</motion.div>
<motion.div
className="text-foreground-muted text-sm lg:text-base"
initial={{ opacity: 0, y: 10 }}
animate={isStatsInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 10 }}
transition={{ delay: index * 0.1 + 0.3, duration: 0.5 }}
>
{stat.label}
</motion.div>
</motion.div>
))}
</motion.div>
{/* Testimonial */} {/* Testimonial */}
<ScrollReveal delay={400}> <motion.div
<div className="card-dark p-8 lg:p-12 max-w-4xl mx-auto"> ref={testimonialRef}
initial="hidden"
animate={isTestimonialInView ? "visible" : "hidden"}
variants={scaleIn}
>
<motion.div
className="card-dark p-8 lg:p-12 max-w-4xl mx-auto"
whileHover={{ y: -5, boxShadow: "0 0 40px rgba(51, 102, 255, 0.3)" }}
transition={{ duration: 0.3 }}
>
<div className="flex flex-col lg:flex-row items-start gap-8"> <div className="flex flex-col lg:flex-row items-start gap-8">
{/* Quote */} {/* Quote */}
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center mb-6"> <motion.div
className="flex items-center mb-6"
initial="hidden"
animate={isTestimonialInView ? "visible" : "hidden"}
variants={staggerContainer}
>
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<Star key={i} className="w-5 h-5 text-neon fill-current" /> <motion.div
key={i}
variants={staggerItem}
whileHover={{ scale: 1.2, rotate: 360 }}
transition={{ duration: 0.4 }}
>
<Star className="w-5 h-5 text-neon fill-current" />
</motion.div>
))} ))}
</div> </motion.div>
<blockquote className="text-lg lg:text-xl text-foreground leading-relaxed mb-6"> <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">
<div className="w-12 h-12 bg-neon/20 rounded-full flex items-center justify-center mr-4"> <motion.div
className="w-12 h-12 bg-neon/20 rounded-full flex items-center justify-center mr-4"
animate={{
boxShadow: [
"0 0 10px rgba(51, 102, 255, 0.3)",
"0 0 20px rgba(51, 102, 255, 0.5)",
"0 0 10px rgba(51, 102, 255, 0.3)"
]
}}
transition={{ duration: 2, repeat: Infinity }}
>
<Users className="w-6 h-6 text-neon" /> <Users className="w-6 h-6 text-neon" />
</div> </motion.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">
@ -93,29 +171,43 @@ const ProofSection = () => {
</div> </div>
</div> </div>
</div> </div>
</div> </motion.div>
</ScrollReveal> </motion.div>
{/* Service area */} {/* Service area */}
<ScrollReveal delay={600}> <motion.div
<div className="mt-16 text-center"> initial={{ opacity: 0, y: 30 }}
<h3 className="font-heading font-semibold text-xl text-foreground mb-6"> animate={isTestimonialInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
Serving businesses throughout the Coastal Bend transition={{ delay: 0.4, duration: 0.6 }}
</h3> className="mt-16 text-center"
>
<h3 className="font-heading font-semibold text-xl text-foreground mb-6">
Serving businesses throughout the Coastal Bend
</h3>
<div className="flex flex-wrap justify-center items-center gap-6 text-foreground-muted"> <motion.div
{[ className="flex flex-wrap justify-center items-center gap-6 text-foreground-muted"
'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass', initial="hidden"
'Rockport', 'Fulton', 'Sinton', 'Mathis' animate={isTestimonialInView ? "visible" : "hidden"}
].map((city) => ( variants={staggerContainer}
<span key={city} className="flex items-center text-sm"> >
<MapPin className="w-3 h-3 mr-1 text-neon" /> {[
{city} 'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass',
</span> 'Rockport', 'Fulton', 'Sinton', 'Mathis'
))} ].map((city) => (
</div> <motion.span
</div> key={city}
</ScrollReveal> className="flex items-center text-sm"
variants={staggerItem}
whileHover={{ scale: 1.1, color: "rgba(51, 102, 255, 1)" }}
transition={{ duration: 0.2 }}
>
<MapPin className="w-3 h-3 mr-1 text-neon" />
{city}
</motion.span>
))}
</motion.div>
</motion.div>
</div> </div>
</section> </section>
); );

View File

@ -1,6 +1,8 @@
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 ScrollReveal from '../ScrollReveal'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { fadeInUp, staggerContainer, staggerItem, cardHover, glowHover } from '@/utils/animations';
const ServicesOverview = () => { const ServicesOverview = () => {
const services = [ const services = [
@ -42,6 +44,11 @@ const ServicesOverview = () => {
} }
]; ];
const headerRef = useRef(null);
const gridRef = useRef(null);
const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" });
const isGridInView = useInView(gridRef, { once: true, margin: "-100px" });
return ( 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 */}
@ -50,7 +57,12 @@ const ServicesOverview = () => {
<div className="absolute bottom-1/4 left-0 w-96 h-96 bg-neon/5 rounded-full blur-3xl"></div> <div className="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">
<ScrollReveal> <motion.div
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{' '}
@ -60,25 +72,42 @@ 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>
</ScrollReveal> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <motion.div
ref={gridRef}
initial="hidden"
animate={isGridInView ? "visible" : "hidden"}
variants={staggerContainer}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{services.map((service, index) => { {services.map((service, index) => {
const Icon = service.icon; const Icon = service.icon;
return ( return (
<ScrollReveal key={service.title} delay={index * 100}> <motion.div key={service.title} variants={staggerItem}>
<div className="card-dark p-8 group hover:shadow-neon transition-all duration-500 hover:-translate-y-1"> <motion.div
className="card-dark p-8 h-full"
whileHover={{
y: -8,
boxShadow: "0 0 30px rgba(51, 102, 255, 0.4)"
}}
transition={{ duration: 0.3 }}
>
{/* Icon */} {/* Icon */}
<div className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center mb-6 group-hover:bg-neon/30 transition-colors"> <motion.div
className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center mb-6"
whileHover={{ rotate: 360, scale: 1.1 }}
transition={{ duration: 0.6 }}
>
<Icon className="w-6 h-6 text-neon" /> <Icon className="w-6 h-6 text-neon" />
</div> </motion.div>
{/* Content */} {/* Content */}
<h3 className="font-heading font-bold text-xl text-foreground mb-4"> <h3 className="font-heading font-bold text-xl text-foreground mb-4">
{service.title} {service.title}
</h3> </h3>
<p className="text-foreground-muted mb-6 leading-relaxed"> <p className="text-foreground-muted mb-6 leading-relaxed">
{service.description} {service.description}
</p> </p>
@ -87,7 +116,18 @@ 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">
<div className="w-1.5 h-1.5 bg-neon rounded-full mr-3"></div> <motion.div
className="w-1.5 h-1.5 bg-neon rounded-full mr-3"
animate={{
scale: [1, 1.3, 1],
opacity: [0.7, 1, 0.7]
}}
transition={{
duration: 2,
repeat: Infinity,
delay: index * 0.2
}}
/>
{feature} {feature}
</li> </li>
))} ))}
@ -96,30 +136,40 @@ const ServicesOverview = () => {
{/* CTA */} {/* CTA */}
<Link <Link
to="/services" to="/services"
className="inline-flex items-center text-neon font-medium hover:text-neon/80 transition-colors group-hover:underline" className="inline-flex items-center text-neon font-medium hover:text-neon/80 transition-colors group"
> >
Learn more Learn more
<svg className="w-4 h-4 ml-1 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <motion.svg
className="w-4 h-4 ml-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
animate={{ x: [0, 3, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
</svg> </motion.svg>
</Link> </Link>
</div> </motion.div>
</ScrollReveal> </motion.div>
); );
})} })}
</div> </motion.div>
{/* Bottom CTA */} {/* Bottom CTA */}
<ScrollReveal delay={600}> <motion.div
<div className="text-center mt-16"> initial={{ opacity: 0, y: 30 }}
<Link animate={isGridInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
to="/services" transition={{ delay: 0.8, duration: 0.6 }}
className="btn-ghost text-lg px-12 py-4" className="text-center mt-16"
> >
View all services <Link
</Link> to="/services"
</div> className="btn-ghost text-lg px-12 py-4"
</ScrollReveal> >
View all services
</Link>
</motion.div>
</div> </div>
</section> </section>
); );

View File

@ -1,6 +1,58 @@
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 ScrollReveal from '../ScrollReveal'; import { motion, useInView, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { useRef, MouseEvent } from 'react';
import { fadeInUp, slideInLeft, slideInRight, buttonHover, buttonTap, cardHover } from '@/utils/animations';
// Optimized 3D Tilt Card Component with reduced effect
const TiltCard = ({ children, className = '' }: { children: React.ReactNode; className?: string }) => {
const cardRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
// Reduced rotation range for subtler effect (10 -> 5 degrees)
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [5, -5]), {
stiffness: 200,
damping: 25,
});
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-5, 5]), {
stiffness: 200,
damping: 25,
});
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (!cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
mouseX.set((e.clientX - centerX) / (rect.width / 2));
mouseY.set((e.clientY - centerY) / (rect.height / 2));
};
const handleMouseLeave = () => {
mouseX.set(0);
mouseY.set(0);
};
return (
<motion.div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
rotateX,
rotateY,
transformStyle: 'preserve-3d',
willChange: 'transform',
}}
className={className}
whileHover={{ scale: 1.01 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
};
const ValuePillars = () => { const ValuePillars = () => {
const pillars = [ const pillars = [
@ -27,13 +79,21 @@ 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">
<ScrollReveal> <motion.div
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{' '}
@ -43,58 +103,96 @@ 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>
</ScrollReveal> </motion.div>
<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 (
<ScrollReveal key={pillar.number} delay={index * 200}> <motion.div
key={pillar.number}
ref={itemRef}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
variants={isReverse ? slideInRight : slideInLeft}
transition={{ delay: 0.2 }}
>
<div className={`flex flex-col ${isReverse ? 'lg:flex-row-reverse' : 'lg:flex-row'} items-center gap-12 lg:gap-16`}> <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">
<span className="text-6xl font-heading font-bold text-neon/30"> <motion.span
className="text-6xl font-heading font-bold text-neon/30"
initial={{ opacity: 0, scale: 0.5 }}
animate={isInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.5 }}
transition={{ delay: 0.4, duration: 0.6, type: "spring" }}
>
{pillar.number} {pillar.number}
</span> </motion.span>
<div className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center"> <motion.div
className="w-12 h-12 bg-neon/20 rounded-xl flex items-center justify-center"
initial={{ opacity: 0, rotate: -180 }}
animate={isInView ? { opacity: 1, rotate: 0 } : { opacity: 0, rotate: -180 }}
transition={{ delay: 0.5, duration: 0.6 }}
>
<Icon className="w-6 h-6 text-neon" /> <Icon className="w-6 h-6 text-neon" />
</div> </motion.div>
</div> </div>
<h3 className="font-heading font-bold text-3xl text-foreground"> <h3 className="font-heading font-bold text-3xl text-foreground">
{pillar.title} {pillar.title}
</h3> </h3>
<p className="text-lg text-foreground-muted leading-relaxed"> <p className="text-lg text-foreground-muted leading-relaxed">
{pillar.description} {pillar.description}
</p> </p>
<Link <motion.div whileHover={buttonHover} whileTap={buttonTap}>
to="/services" <Link
className="btn-ghost group flex items-center space-x-2 w-fit" to="/services"
onClick={() => window.scrollTo(0, 0)} className="btn-ghost group flex items-center space-x-2 w-fit"
> onClick={() => window.scrollTo(0, 0)}
<span>Learn more</span> >
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" /> <span>Learn more</span>
</Link> <motion.div
animate={{ x: [0, 3, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<ArrowRight className="w-4 h-4" />
</motion.div>
</Link>
</motion.div>
</div> </div>
{/* Image */} {/* Image with 3D Tilt Effect */}
<div className="flex-1 parallax"> <TiltCard className="flex-1">
<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">
<img <div className="relative overflow-hidden rounded-xl">
src={pillar.image} <motion.img
alt={pillar.title} src={pillar.image}
className="w-full h-64 lg:h-80 object-cover rounded-xl transition-transform duration-500 group-hover:scale-105" alt={pillar.title}
loading="lazy" className="w-full h-64 lg:h-80 object-cover"
/> whileHover={{ scale: 1.05 }}
transition={{ duration: 0.4, ease: "easeOut" }}
loading="lazy"
style={{ willChange: 'transform' }}
/>
{/* Simplified shine effect on hover */}
<motion.div
className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/8 to-transparent pointer-events-none"
initial={{ x: '-100%', y: '-100%' }}
whileHover={{ x: '100%', y: '100%' }}
transition={{ duration: 0.5, ease: "easeOut" }}
/>
</div>
</div> </div>
</div> </TiltCard>
</div> </div>
</ScrollReveal> </motion.div>
); );
})} })}
</div> </div>

View File

@ -88,11 +88,36 @@
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;
} }
*, *,
*::before, *::before,
*::after { *::after {
@ -101,6 +126,29 @@
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 {
@ -148,6 +196,26 @@
@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 {
@ -155,16 +223,116 @@
@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;
}
} }

View File

@ -372,6 +372,47 @@ const Contact = () => {
</ScrollReveal> </ScrollReveal>
</div> </div>
</section> </section>
{/* Service Area Map */}
<section className="py-16 bg-background-deep">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal>
<div className="text-center mb-8">
<h2 className="font-heading font-bold text-3xl text-foreground mb-4">
Our Service Area
</h2>
<p className="text-foreground-muted max-w-2xl mx-auto">
Proudly serving Corpus Christi, Portland, Rockport, Aransas Pass, Kingsville, Port Aransas, and the entire Coastal Bend region.
</p>
</div>
<div className="card-dark p-2 overflow-hidden">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d113726.84791849895!2d-97.48659164550781!3d27.800587899999997!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x8668fa3c40818e93%3A0x4e3c0a1c2bef9c65!2sCorpus%20Christi%2C%20TX!5e0!3m2!1sen!2sus!4v1736364000000!5m2!1sen!2sus"
width="100%"
height="450"
style={{ border: 0, borderRadius: '1rem' }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Bay Area Affiliates Service Area - Corpus Christi and Coastal Bend"
aria-label="Google Maps showing our service area in Corpus Christi and the Coastal Bend region"
/>
</div>
<div className="mt-8 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville', 'Port Aransas'].map((city) => (
<div key={city} className="text-center">
<div className="card-dark p-3">
<MapPin className="w-5 h-5 text-neon mx-auto mb-1" />
<p className="text-xs text-foreground font-medium">{city}</p>
</div>
</div>
))}
</div>
</ScrollReveal>
</div>
</section>
</main> </main>
<Footer /> <Footer />

View File

@ -11,7 +11,7 @@ const Index = () => {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<Navigation /> <Navigation />
<main> <main id="main-content" role="main">
<HeroSection /> <HeroSection />
<ValuePillars /> <ValuePillars />
<ProcessTimeline /> <ProcessTimeline />

210
src/utils/animations.ts Normal file
View File

@ -0,0 +1,210 @@
import { Variants } from 'framer-motion';
// Smooth easing curves
export const easing = {
smooth: [0.6, 0.01, 0.05, 0.95],
snappy: [0.25, 0.46, 0.45, 0.94],
bouncy: [0.68, -0.55, 0.265, 1.55],
elegant: [0.43, 0.13, 0.23, 0.96],
};
// Hero section animations
export const heroVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.3,
},
},
};
export const heroItemVariants: Variants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.8,
ease: easing.elegant,
},
},
};
// Fade in up animation
export const fadeInUp: Variants = {
hidden: {
opacity: 0,
y: 60,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.7,
ease: easing.elegant,
},
},
};
// Scale in animation
export const scaleIn: Variants = {
hidden: {
opacity: 0,
scale: 0.8,
},
visible: {
opacity: 1,
scale: 1,
transition: {
duration: 0.6,
ease: easing.smooth,
},
},
};
// Slide in from left
export const slideInLeft: Variants = {
hidden: {
opacity: 0,
x: -60,
},
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.7,
ease: easing.elegant,
},
},
};
// Slide in from right
export const slideInRight: Variants = {
hidden: {
opacity: 0,
x: 60,
},
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.7,
ease: easing.elegant,
},
},
};
// Stagger container
export const staggerContainer: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.12,
delayChildren: 0.1,
},
},
};
// Stagger item
export const staggerItem: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: easing.elegant,
},
},
};
// Button hover animation
export const buttonHover = {
scale: 1.02,
transition: {
duration: 0.2,
ease: easing.snappy,
},
};
export const buttonTap = {
scale: 0.98,
};
// Card hover animation
export const cardHover = {
y: -8,
transition: {
duration: 0.3,
ease: easing.smooth,
},
};
// Glow effect
export const glowHover = {
boxShadow: '0 0 30px rgba(51, 102, 255, 0.6)',
transition: {
duration: 0.3,
},
};
// Navigation animation
export const navVariants: Variants = {
hidden: {
y: -100,
opacity: 0,
},
visible: {
y: 0,
opacity: 1,
transition: {
duration: 0.6,
ease: easing.elegant,
},
},
};
// Page transition
export const pageTransition = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
transition: { duration: 0.4, ease: easing.elegant },
};
// Parallax scroll effect
export const parallaxScroll = (scrollY: number, factor: number = 0.5) => ({
y: scrollY * factor,
transition: { type: 'tween', ease: 'linear', duration: 0 },
});
// Scroll reveal with intersection observer
export const scrollRevealVariants: Variants = {
hidden: {
opacity: 0,
y: 50,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.7,
ease: easing.elegant,
},
},
};
// Magnetic button effect (advanced)
export const magneticEffect = (x: number, y: number, strength: number = 0.3) => ({
x: x * strength,
y: y * strength,
transition: {
type: 'spring',
stiffness: 150,
damping: 15,
mass: 0.1,
},
});

View File

@ -0,0 +1,100 @@
import { onCLS, onFCP, onLCP, onTTFB, onINP, type Metric } from 'web-vitals';
function sendToAnalytics(metric: Metric) {
// Log to console in development
if (import.meta.env.DEV) {
console.log('Web Vitals:', {
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
});
}
// Send to Google Analytics if available
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
non_interaction: true,
});
}
// Send to custom analytics endpoint if needed
if (import.meta.env.PROD && import.meta.env.VITE_ANALYTICS_ENDPOINT) {
fetch(import.meta.env.VITE_ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metric: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
}),
keepalive: true,
}).catch((error) => {
console.error('Failed to send web vitals:', error);
});
}
}
export function reportWebVitals() {
// Core Web Vitals
onCLS(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics); // Replaces deprecated FID
// Other important metrics
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}
// Performance observer for additional metrics
export function observePerformance() {
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) {
return;
}
// Observe long tasks (blocking the main thread)
try {
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long Task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
});
}
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
} catch (e) {
// Long task API not supported
}
// Observe layout shifts
try {
const layoutShiftObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if ((entry as any).hadRecentInput) continue;
const value = (entry as any).value;
if (value > 0.1) {
console.warn('Large Layout Shift:', {
value,
startTime: entry.startTime,
sources: (entry as any).sources,
});
}
}
});
layoutShiftObserver.observe({ entryTypes: ['layout-shift'] });
} catch (e) {
// Layout shift API not supported
}
}