diff --git a/index.html b/index.html index 4e52630..7b6b7c7 100644 --- a/index.html +++ b/index.html @@ -3,9 +3,9 @@ - Managed IT Services Corpus Christi | Bay Area Affiliates - - + IT Support Corpus Christi ⚡ 24/7 Managed Services | Bay Area Affiliates + + @@ -211,10 +211,22 @@ } + + + + + + - - + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 64a9355..7d91188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.24.12", "gsap": "^3.13.0", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", @@ -73,10 +74,13 @@ "globals": "^15.15.0", "lovable-tagger": "^1.1.9", "postcss": "^8.5.6", + "sharp": "^0.34.5", "tailwindcss": "^3.4.17", + "terser": "^5.44.1", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", - "vite": "^5.4.19" + "vite": "^5.4.19", + "web-vitals": "^5.1.0" } }, "node_modules/@alloc/quick-lru": { @@ -150,6 +154,17 @@ "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": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -859,6 +874,496 @@ "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": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -908,6 +1413,17 @@ "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": { "version": "1.5.5", "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_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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3868,6 +4391,16 @@ "dev": true, "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": { "version": "1.1.0", "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" } }, + "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": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4874,6 +5434,21 @@ "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": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5683,9 +6258,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -5695,6 +6270,51 @@ "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": { "version": "2.0.0", "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" } }, + "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5747,6 +6377,17 @@ "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": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5959,6 +6600,32 @@ "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6705,6 +7372,13 @@ "@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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 73e5897..7f4a51f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.24.12", "gsap": "^3.13.0", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", @@ -76,9 +77,12 @@ "globals": "^15.15.0", "lovable-tagger": "^1.1.9", "postcss": "^8.5.6", + "sharp": "^0.34.5", "tailwindcss": "^3.4.17", + "terser": "^5.44.1", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", - "vite": "^5.4.19" + "vite": "^5.4.19", + "web-vitals": "^5.1.0" } } diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..c43d80b --- /dev/null +++ b/public/_headers @@ -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 diff --git a/public/about_banner.avif b/public/about_banner.avif new file mode 100644 index 0000000..9d2a404 Binary files /dev/null and b/public/about_banner.avif differ diff --git a/public/about_banner.png b/public/about_banner.png index 52ddf19..7f8db17 100644 Binary files a/public/about_banner.png and b/public/about_banner.png differ diff --git a/public/about_banner.webp b/public/about_banner.webp new file mode 100644 index 0000000..709f599 Binary files /dev/null and b/public/about_banner.webp differ diff --git a/public/blog_banner.avif b/public/blog_banner.avif new file mode 100644 index 0000000..d4c05a0 Binary files /dev/null and b/public/blog_banner.avif differ diff --git a/public/blog_banner.png b/public/blog_banner.png index 4a08e91..166b369 100644 Binary files a/public/blog_banner.png and b/public/blog_banner.png differ diff --git a/public/blog_banner.webp b/public/blog_banner.webp new file mode 100644 index 0000000..49d3714 Binary files /dev/null and b/public/blog_banner.webp differ diff --git a/public/contact_banner.avif b/public/contact_banner.avif new file mode 100644 index 0000000..f2e1c20 Binary files /dev/null and b/public/contact_banner.avif differ diff --git a/public/contact_banner.png b/public/contact_banner.png index f2e5a76..d19b933 100644 Binary files a/public/contact_banner.png and b/public/contact_banner.png differ diff --git a/public/contact_banner.webp b/public/contact_banner.webp new file mode 100644 index 0000000..dbd3223 Binary files /dev/null and b/public/contact_banner.webp differ diff --git a/public/faster_happier.avif b/public/faster_happier.avif new file mode 100644 index 0000000..606be47 Binary files /dev/null and b/public/faster_happier.avif differ diff --git a/public/faster_happier.png b/public/faster_happier.png index 19fbb00..7fadd92 100644 Binary files a/public/faster_happier.png and b/public/faster_happier.png differ diff --git a/public/faster_happier.webp b/public/faster_happier.webp new file mode 100644 index 0000000..afb1e9e Binary files /dev/null and b/public/faster_happier.webp differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..0f3c08d --- /dev/null +++ b/public/manifest.json @@ -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" +} diff --git a/public/serverroom.avif b/public/serverroom.avif new file mode 100644 index 0000000..9e73312 Binary files /dev/null and b/public/serverroom.avif differ diff --git a/public/serverroom.png b/public/serverroom.png index 958cb1a..3561a84 100644 Binary files a/public/serverroom.png and b/public/serverroom.png differ diff --git a/public/serverroom.webp b/public/serverroom.webp new file mode 100644 index 0000000..712e403 Binary files /dev/null and b/public/serverroom.webp differ diff --git a/public/service_background.avif b/public/service_background.avif new file mode 100644 index 0000000..59fa6cb Binary files /dev/null and b/public/service_background.avif differ diff --git a/public/service_background.png b/public/service_background.png index 9c89339..e1ede1b 100644 Binary files a/public/service_background.png and b/public/service_background.png differ diff --git a/public/service_background.webp b/public/service_background.webp new file mode 100644 index 0000000..5316a45 Binary files /dev/null and b/public/service_background.webp differ diff --git a/scripts/optimize-images.cjs b/scripts/optimize-images.cjs new file mode 100644 index 0000000..4dcc775 --- /dev/null +++ b/scripts/optimize-images.cjs @@ -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)); diff --git a/src/App.tsx b/src/App.tsx index 2498efa..760bfff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,12 @@ import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; -import { lazy, Suspense } from "react"; +import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; +import { lazy, Suspense, useEffect } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { reportWebVitals, observePerformance } from "@/utils/reportWebVitals"; +import { pageTransition } from "@/utils/animations"; +import ExitIntentPopup from "@/components/ExitIntentPopup"; // Eager load critical pages for better initial performance import Index from "./pages/Index"; @@ -26,42 +30,101 @@ const NetworkAttachedStorage = lazy(() => import("./pages/NetworkAttachedStorage const queryClient = new QueryClient(); -// Loading fallback component +// Loading fallback component with animation const PageLoader = () => ( -
+
-
-

Loading...

+ + + Loading... +
-
+ ); +// Animated page wrapper +const AnimatedPage = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; + +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 ( + <> + + + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + + ); +}; + const App = () => ( - }> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - + diff --git a/src/components/ExitIntentPopup.tsx b/src/components/ExitIntentPopup.tsx new file mode 100644 index 0000000..fd58671 --- /dev/null +++ b/src/components/ExitIntentPopup.tsx @@ -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 */} +
+ + {/* Popup */} +
+
+ {/* Close button */} + + + {/* Content */} +
+
+ +
+ +

+ Wait! Don't leave empty-handed +

+ +

+ Download our free Windows 11 Migration Checklist — + a $299 value guide to help you prepare for the upgrade. +

+ + {/* Key benefits */} +
    +
  • + + + + Hardware compatibility check (avoid costly mistakes) +
  • +
  • + + + + Step-by-step migration timeline +
  • +
  • + + + + Security & compliance considerations +
  • +
  • + + + + Rollback plan (just in case) +
  • +
+ + {/* CTA */} + + + Download Free Checklist + + +

+ No spam. No credit card required. Instant access. +

+ + {/* Close link */} + +
+
+
+ + ); +}; + +export default ExitIntentPopup; diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 987f0cd..cc07a08 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Menu, X } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { navVariants, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations'; const Navigation = () => { const [isOpen, setIsOpen] = useState(false); @@ -27,87 +29,173 @@ const Navigation = () => { const isActive = (path: string) => location.pathname === path; return ( - + + ); }; diff --git a/src/components/ScrollReveal.tsx b/src/components/ScrollReveal.tsx index 1fdc004..a76687a 100644 --- a/src/components/ScrollReveal.tsx +++ b/src/components/ScrollReveal.tsx @@ -1,8 +1,7 @@ -import { ReactNode, useLayoutEffect, useRef } from 'react'; -import gsap from 'gsap'; -import { ScrollTrigger } from 'gsap/ScrollTrigger'; - -gsap.registerPlugin(ScrollTrigger); +import { ReactNode } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { scrollRevealVariants } from '@/utils/animations'; type ScrollRevealProps = { children: ReactNode; @@ -11,38 +10,20 @@ type ScrollRevealProps = { }; const ScrollReveal = ({ children, delay = 0, className = '' }: ScrollRevealProps) => { - const elementRef = useRef(null); - - useLayoutEffect(() => { - const element = elementRef.current; - if (!element) return; - - const ctx = gsap.context(() => { - gsap.fromTo( - element, - { autoAlpha: 0, y: 32 }, - { - autoAlpha: 1, - y: 0, - duration: 0.8, - ease: 'power3.out', - delay: delay / 1000, - scrollTrigger: { - trigger: element, - start: 'top 85%', - once: true, - }, - } - ); - }, element); - - return () => ctx.revert(); - }, [delay]); + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: "-100px" }); return ( -
+ {children} -
+ ); }; diff --git a/src/components/home/BackgroundAnimations.tsx b/src/components/home/BackgroundAnimations.tsx new file mode 100644 index 0000000..9e6c928 --- /dev/null +++ b/src/components/home/BackgroundAnimations.tsx @@ -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 ( +
+ {/* Simplified Static Grid Background - no animation for better performance */} +
+ + {/* SVG Container for Lines and Nodes - optimized */} + + + {/* Simplified Connection Lines - static for better performance */} + {connectionLines.map((line) => ( + + ))} + + {/* Reduced Vertical Data Streams (8 -> 4) */} + {[...Array(4)].map((_, i) => ( + + ))} + + {/* Simplified Circuit Nodes - reduced animation */} + {circuitNodes.map((node) => ( + + {/* Core node with simple pulse */} + + + ))} + + + {/* Optimized Floating Data Particles with GPU acceleration */} + {particles.map((particle) => ( + + ))} + + {/* Simplified Static Scanline Effect - no animation */} +
+ + {/* Reduced Radial Glows (3 -> 2) with simpler animation */} + {[ + { x: 25, y: 40 }, + { x: 75, y: 60 }, + ].map((pos, i) => ( + + ))} + + {/* Static Corner Accent Glows - no blur for better performance */} +
+
+
+ ); +}; + +export default BackgroundAnimations; diff --git a/src/components/home/CTASection.tsx b/src/components/home/CTASection.tsx index 48c39bb..92779b2 100644 --- a/src/components/home/CTASection.tsx +++ b/src/components/home/CTASection.tsx @@ -1,6 +1,8 @@ import { ArrowRight, Clock, DollarSign, Phone } from 'lucide-react'; import { Link } from 'react-router-dom'; -import ScrollReveal from '../ScrollReveal'; +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { fadeInUp, staggerContainer, staggerItem, buttonHover, buttonTap } from '@/utils/animations'; const CTASection = () => { const faqs = [ @@ -21,6 +23,13 @@ const CTASection = () => { } ]; + const headerRef = useRef(null); + const ctaRef = useRef(null); + const faqRef = useRef(null); + const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" }); + const isCtaInView = useInView(ctaRef, { once: true, margin: "-100px" }); + const isFaqInView = useInView(faqRef, { once: true, margin: "-100px" }); + return (
{/* Background decoration */} @@ -29,88 +38,141 @@ const CTASection = () => {
- +

- Ready for reliable IT? + Ready for + reliable IT? +

- +

- 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.

-
+ {/* Primary CTAs */} - -
+ + Book a 20-minute assessment - + + + - + + + Send a message -
-
+ + {/* Micro FAQ */} - -
- {faqs.map((faq, index) => { - const Icon = faq.icon; - - return ( -
-
-
- -
-
-

- {faq.question} -

-

- {faq.answer} -

-
+ + {faqs.map((faq, index) => { + const Icon = faq.icon; + + return ( + + + + + +
+

+ {faq.question} +

+

+ {faq.answer} +

-
- ); - })} -
- + + + ); + })} + {/* Contact info */} - -
-

- Ready to talk? We're here to help. -

- + +

+ Ready to talk? We're here to help. +

+
+ + + (361) 555-0123 + + | + + info@bayareaaffiliates.com +
- +
diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx index cc0dcb4..e60fb03 100644 --- a/src/components/home/HeroSection.tsx +++ b/src/components/home/HeroSection.tsx @@ -1,87 +1,165 @@ import { ArrowRight, Play } from 'lucide-react'; import { Link } from 'react-router-dom'; -import { useEffect, useRef } from 'react'; -import heroNetwork from '@/assets/hero-network.jpg'; +import { motion, useScroll, useTransform } from 'framer-motion'; +import { heroVariants, heroItemVariants, buttonHover, buttonTap, easing } from '@/utils/animations'; +import BackgroundAnimations from './BackgroundAnimations'; const HeroSection = () => { - const imageRef = useRef(null); + const { scrollY } = useScroll(); - useEffect(() => { - const handleScroll = () => { - if (imageRef.current) { - const scrolled = window.pageYOffset; - const parallax = scrolled * 0.5; - imageRef.current.style.transform = `translateY(${parallax}px) scale(1.1)`; - } - }; - - window.addEventListener('scroll', handleScroll, { passive: true }); - return () => window.removeEventListener('scroll', handleScroll); - }, []); + // Smooth parallax effect with Framer Motion + const y = useTransform(scrollY, [0, 500], [0, 150]); + const opacity = useTransform(scrollY, [0, 300], [1, 0]); return (
- {/* Background image with parallax */} -
- Modern IT infrastructure with network connections - {/* Dark overlay */} -
-
+ {/* Background image with smooth parallax */} + + + + + + - {/* Main content */} -
+ {/* Animated background effects */} + + + {/* Darker overlay for better text contrast */} +
+ + + {/* Main content with staggered animations */} +
{/* Badge */} -
- + + Serving the Coastal Bend since 2010 -
+ {/* Main headline */} -

+ Modern IT that keeps your{' '} - business moving -

+ + business moving + + {/* Subheadline */} -

+ From fast devices to secure remote access and resilient networks — we design, run and protect your tech so you can focus on growth. -

+ - {/* CTA buttons */} -
- + - Get in touch - - + + Get in touch + + + + + - -
+ + + + See our services + + +
-
+
- {/* Scroll indicator */} -
+ {/* Animated scroll indicator */} +
-
+
-
+
); diff --git a/src/components/home/ProcessTimeline.tsx b/src/components/home/ProcessTimeline.tsx index d67a37c..6afbb24 100644 --- a/src/components/home/ProcessTimeline.tsx +++ b/src/components/home/ProcessTimeline.tsx @@ -1,10 +1,20 @@ import { useEffect, useRef, useState } from 'react'; import { Search, Shield, Cog, Zap } from 'lucide-react'; -import ScrollReveal from '../ScrollReveal'; +import { motion, useInView, useScroll, useTransform } from 'framer-motion'; +import { fadeInUp, scaleIn } from '@/utils/animations'; const ProcessTimeline = () => { const [activeStep, setActiveStep] = useState(0); const sectionRef = useRef(null); + const headerRef = useRef(null); + const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" }); + + // Smooth scroll-based timeline progress + const { scrollYProgress } = useScroll({ + target: sectionRef, + offset: ["start end", "end start"] + }); + const timelineProgress = useTransform(scrollYProgress, [0.2, 0.8], [0, 100]); const steps = [ { @@ -63,7 +73,12 @@ const ProcessTimeline = () => {
- +

How we transform your IT @@ -72,14 +87,14 @@ const ProcessTimeline = () => { Our proven four-phase methodology ensures systematic improvement and lasting results.

-
+
{/* Timeline line */}
-
`${value}%`) }} />
@@ -89,17 +104,35 @@ const ProcessTimeline = () => { const Icon = step.icon; const isActive = index <= activeStep; const isEven = index % 2 === 0; - + const stepRef = useRef(null); + const isInView = useInView(stepRef, { once: true, margin: "-150px" }); + return ( - +
{/* Step content */} -
-
+ +
Step {index + 1} @@ -114,24 +147,34 @@ const ProcessTimeline = () => {

{step.details}

-
-
+ + {/* Timeline dot */}
-
+ -
+
{/* Spacer for layout */}
- + ); })}
diff --git a/src/components/home/ProofSection.tsx b/src/components/home/ProofSection.tsx index e99987a..9405e65 100644 --- a/src/components/home/ProofSection.tsx +++ b/src/components/home/ProofSection.tsx @@ -1,6 +1,8 @@ import { MapPin, Star, Users } from 'lucide-react'; -import ScrollReveal from '../ScrollReveal'; +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; import CountUpNumber from '../CountUpNumber'; +import { fadeInUp, scaleIn, staggerContainer, staggerItem } from '@/utils/animations'; const ProofSection = () => { const stats = [ @@ -17,6 +19,13 @@ const ProofSection = () => { company: "Coastal Medical Group" }; + const headerRef = useRef(null); + const statsRef = useRef(null); + const testimonialRef = useRef(null); + const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" }); + const isStatsInView = useInView(statsRef, { once: true, margin: "-100px" }); + const isTestimonialInView = useInView(testimonialRef, { once: true, margin: "-100px" }); + return (
{/* Background decoration */} @@ -24,12 +33,27 @@ const ProofSection = () => {
- +
-
+ Proudly serving the Coastal Bend -
+

Local expertise for{' '} @@ -42,48 +66,102 @@ const ProofSection = () => { tailored solutions that work in the real world.

-
+ {/* Stats */} - -
- {stats.map((stat, index) => ( -
-
- -
-
- {stat.label} -
-
- ))} -
-
+ + {stats.map((stat, index) => ( + + + + + + {stat.label} + + + ))} + {/* Testimonial */} - -
+ +
{/* Quote */}
-
+ {[...Array(5)].map((_, i) => ( - + + + ))} -
+
"{testimonial.quote}"
-
+ -
+
{testimonial.author}
@@ -93,29 +171,43 @@ const ProofSection = () => {
-
- + + {/* Service area */} - -
-

- Serving businesses throughout the Coastal Bend -

+ +

+ Serving businesses throughout the Coastal Bend +

-
- {[ - 'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass', - 'Rockport', 'Fulton', 'Sinton', 'Mathis' - ].map((city) => ( - - - {city} - - ))} -
-
-
+ + {[ + 'Corpus Christi', 'Portland', 'Ingleside', 'Aransas Pass', + 'Rockport', 'Fulton', 'Sinton', 'Mathis' + ].map((city) => ( + + + {city} + + ))} + +
); diff --git a/src/components/home/ServicesOverview.tsx b/src/components/home/ServicesOverview.tsx index 69ec37a..4c35d84 100644 --- a/src/components/home/ServicesOverview.tsx +++ b/src/components/home/ServicesOverview.tsx @@ -1,6 +1,8 @@ import { Monitor, Wifi, Cloud, Shield, Database, Settings } from 'lucide-react'; import { Link } from 'react-router-dom'; -import ScrollReveal from '../ScrollReveal'; +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { fadeInUp, staggerContainer, staggerItem, cardHover, glowHover } from '@/utils/animations'; const ServicesOverview = () => { const services = [ @@ -42,6 +44,11 @@ const ServicesOverview = () => { } ]; + const headerRef = useRef(null); + const gridRef = useRef(null); + const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" }); + const isGridInView = useInView(gridRef, { once: true, margin: "-100px" }); + return (
{/* Background decoration */} @@ -50,7 +57,12 @@ const ServicesOverview = () => {
- +

Complete IT solutions for{' '} @@ -60,25 +72,42 @@ const ServicesOverview = () => { From desktop support to enterprise infrastructure — we've got your technology covered.

-
+ -
+ {services.map((service, index) => { const Icon = service.icon; - + return ( - -
+ + {/* Icon */} -
+ -
+
{/* Content */}

{service.title}

- +

{service.description}

@@ -87,7 +116,18 @@ const ServicesOverview = () => {
    {service.features.map((feature) => (
  • -
    + {feature}
  • ))} @@ -96,30 +136,40 @@ const ServicesOverview = () => { {/* CTA */} Learn more - + - + -
-
+
+ ); })} -
+ {/* Bottom CTA */} - -
- - View all services - -
-
+ + + View all services + +
); diff --git a/src/components/home/ValuePillars.tsx b/src/components/home/ValuePillars.tsx index 88ca479..aa46e34 100644 --- a/src/components/home/ValuePillars.tsx +++ b/src/components/home/ValuePillars.tsx @@ -1,6 +1,58 @@ import { Shield, Zap, Users, ArrowRight } from 'lucide-react'; import { Link } from 'react-router-dom'; -import ScrollReveal from '../ScrollReveal'; +import { motion, useInView, useMotionValue, useSpring, useTransform } from 'framer-motion'; +import { useRef, MouseEvent } from 'react'; +import { fadeInUp, slideInLeft, slideInRight, buttonHover, buttonTap, cardHover } from '@/utils/animations'; + +// Optimized 3D Tilt Card Component with reduced effect +const TiltCard = ({ children, className = '' }: { children: React.ReactNode; className?: string }) => { + const cardRef = useRef(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) => { + 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 ( + + {children} + + ); +}; const ValuePillars = () => { const pillars = [ @@ -27,13 +79,21 @@ const ValuePillars = () => { }, ]; + const headerRef = useRef(null); + const isHeaderInView = useInView(headerRef, { once: true, margin: "-100px" }); + return (
{/* Background decoration */}
- +
- +

Why teams choose us for{' '} @@ -43,58 +103,96 @@ const ValuePillars = () => { We handle the complexity so you can focus on what you do best.

-
+
{pillars.map((pillar, index) => { const Icon = pillar.icon; const isReverse = index % 2 === 1; - + const itemRef = useRef(null); + const isInView = useInView(itemRef, { once: true, margin: "-100px" }); + return ( - +
{/* Content */}
- + {pillar.number} - -
+ + -
+
- +

{pillar.title}

- +

{pillar.description}

- - window.scrollTo(0, 0)} - > - Learn more - - + + + window.scrollTo(0, 0)} + > + Learn more + + + + +
- {/* Image */} -
+ {/* Image with 3D Tilt Effect */} +
- {pillar.title} +
+ + {/* Simplified shine effect on hover */} + +
-
+
-
+ ); })}
diff --git a/src/index.css b/src/index.css index fb1bdcf..0123f34 100644 --- a/src/index.css +++ b/src/index.css @@ -88,11 +88,36 @@ scroll-behavior: smooth; } + /* Accessibility: Focus visible styles */ + *:focus-visible { + outline: 2px solid hsl(var(--neon)); + outline-offset: 3px; + border-radius: 4px; + } + + /* Skip link for keyboard navigation */ + .skip-link { + position: absolute; + top: -40px; + left: 0; + background: hsl(var(--neon)); + color: hsl(var(--background)); + padding: 8px 16px; + text-decoration: none; + z-index: 100; + font-weight: 600; + border-radius: 0 0 8px 0; + } + + .skip-link:focus { + top: 0; + } + @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - + *, *::before, *::after { @@ -101,6 +126,29 @@ transition-duration: 0.01ms !important; } } + + /* Screen reader only class */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } + + .sr-only:focus, + .sr-only:active { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; + } } @layer components { @@ -148,6 +196,26 @@ @apply rounded-[var(--radius)] px-8 py-4 font-semibold; @apply transition-all duration-300 ease-out; @apply shadow-[0_0_0_1px_hsl(var(--neon))] hover:shadow-[0_0_20px_hsl(var(--neon)/0.5)]; + position: relative; + overflow: hidden; + } + + .btn-neon::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; + } + + .btn-neon:hover::before { + width: 300px; + height: 300px; } .btn-ghost { @@ -155,16 +223,116 @@ @apply rounded-[var(--radius)] px-8 py-4 font-semibold; @apply transition-all duration-300 ease-out; @apply hover:shadow-[0_0_15px_hsl(var(--neon)/0.3)]; + position: relative; + overflow: hidden; + } + + .btn-ghost::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: linear-gradient(to right, transparent, hsl(var(--neon)), transparent); + transform: translateX(-100%); + transition: transform 0.5s ease-out; + } + + .btn-ghost:hover::after { + transform: translateX(100%); } /* Card styles */ .card-dark { @apply bg-card border border-card-border rounded-[var(--radius-lg)]; @apply backdrop-blur-sm shadow-[var(--shadow-card)]; + position: relative; + overflow: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + } + + .card-dark::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: linear-gradient(45deg, + transparent 30%, + hsl(var(--neon) / 0.1) 50%, + transparent 70%); + border-radius: inherit; + opacity: 0; + transition: opacity 0.4s ease; + z-index: -1; + } + + .card-dark:hover::before { + opacity: 1; + } + + .card-dark:hover { + transform: translateY(-4px); + border-color: hsl(var(--neon) / 0.3); } /* Typography helpers */ .text-balance { text-wrap: balance; } + + /* Smooth focus ring for better UX */ + .focus-ring { + @apply focus:outline-none focus:ring-2 focus:ring-neon focus:ring-offset-2 focus:ring-offset-background; + transition: all 0.2s ease; + } + + /* Magnetic hover effect for interactive elements */ + .magnetic-hover { + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); + } + + .magnetic-hover:hover { + transform: translateY(-2px); + } + + /* Glassmorphism effect */ + .glass { + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + /* Shimmer effect for loading states */ + @keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } + } + + .shimmer { + position: relative; + overflow: hidden; + } + + .shimmer::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(51, 102, 255, 0.1), + transparent + ); + animation: shimmer 2s infinite; + } } \ No newline at end of file diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx index 97bb198..22d71d0 100644 --- a/src/pages/Contact.tsx +++ b/src/pages/Contact.tsx @@ -372,6 +372,47 @@ const Contact = () => {
+ + {/* Service Area Map */} +
+
+ +
+

+ Our Service Area +

+

+ Proudly serving Corpus Christi, Portland, Rockport, Aransas Pass, Kingsville, Port Aransas, and the entire Coastal Bend region. +

+
+ +
+