diff --git a/package-lock.json b/package-lock.json
index 35fdafc..5bf3771 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -76,6 +76,7 @@
"globals": "^15.15.0",
"lovable-tagger": "^1.1.10",
"postcss": "^8.5.6",
+ "sharp": "^0.34.4",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
@@ -153,6 +154,17 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
+ "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -845,6 +857,456 @@
"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.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz",
+ "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz",
+ "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz",
+ "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz",
+ "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz",
+ "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz",
+ "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz",
+ "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==",
+ "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-s390x": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz",
+ "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz",
+ "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz",
+ "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
+ "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
+ "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.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz",
+ "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz",
+ "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz",
+ "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz",
+ "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
+ "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz",
+ "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
+ "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
+ "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.3"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz",
+ "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.5.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.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz",
+ "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==",
+ "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.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz",
+ "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==",
+ "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.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
+ "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
+ "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",
@@ -4282,6 +4744,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/detect-libc": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
+ "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
+ "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",
@@ -6567,6 +7039,49 @@
"node": ">=10"
}
},
+ "node_modules/sharp": {
+ "version": "0.34.4",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
+ "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.0",
+ "semver": "^7.7.2"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.4",
+ "@img/sharp-darwin-x64": "0.34.4",
+ "@img/sharp-libvips-darwin-arm64": "1.2.3",
+ "@img/sharp-libvips-darwin-x64": "1.2.3",
+ "@img/sharp-libvips-linux-arm": "1.2.3",
+ "@img/sharp-libvips-linux-arm64": "1.2.3",
+ "@img/sharp-libvips-linux-ppc64": "1.2.3",
+ "@img/sharp-libvips-linux-s390x": "1.2.3",
+ "@img/sharp-libvips-linux-x64": "1.2.3",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.3",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.3",
+ "@img/sharp-linux-arm": "0.34.4",
+ "@img/sharp-linux-arm64": "0.34.4",
+ "@img/sharp-linux-ppc64": "0.34.4",
+ "@img/sharp-linux-s390x": "0.34.4",
+ "@img/sharp-linux-x64": "0.34.4",
+ "@img/sharp-linuxmusl-arm64": "0.34.4",
+ "@img/sharp-linuxmusl-x64": "0.34.4",
+ "@img/sharp-wasm32": "0.34.4",
+ "@img/sharp-win32-arm64": "0.34.4",
+ "@img/sharp-win32-ia32": "0.34.4",
+ "@img/sharp-win32-x64": "0.34.4"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 4229319..30519f3 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "optimize-images": "node scripts/optimize-images.js"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
@@ -79,6 +80,7 @@
"globals": "^15.15.0",
"lovable-tagger": "^1.1.10",
"postcss": "^8.5.6",
+ "sharp": "^0.34.4",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
diff --git a/public/images/projects/cloud_1.webp b/public/images/projects/cloud_1.webp
new file mode 100644
index 0000000..77643bf
Binary files /dev/null and b/public/images/projects/cloud_1.webp differ
diff --git a/public/images/projects/cub3d.webp b/public/images/projects/cub3d.webp
new file mode 100644
index 0000000..8c1d0b8
Binary files /dev/null and b/public/images/projects/cub3d.webp differ
diff --git a/public/images/projects/homemade_nas.webp b/public/images/projects/homemade_nas.webp
new file mode 100644
index 0000000..19824a3
Binary files /dev/null and b/public/images/projects/homemade_nas.webp differ
diff --git a/public/images/projects/minishell.webp b/public/images/projects/minishell.webp
new file mode 100644
index 0000000..145393e
Binary files /dev/null and b/public/images/projects/minishell.webp differ
diff --git a/public/images/projects/pong.webp b/public/images/projects/pong.webp
new file mode 100644
index 0000000..979a775
Binary files /dev/null and b/public/images/projects/pong.webp differ
diff --git a/public/images/sites/avopieces/mookup/3-devices-black (1).webp b/public/images/sites/avopieces/mookup/3-devices-black (1).webp
new file mode 100644
index 0000000..ce79337
Binary files /dev/null and b/public/images/sites/avopieces/mookup/3-devices-black (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/3-devices-white (1).webp b/public/images/sites/avopieces/mookup/3-devices-white (1).webp
new file mode 100644
index 0000000..03b221b
Binary files /dev/null and b/public/images/sites/avopieces/mookup/3-devices-white (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/desktop (1).webp b/public/images/sites/avopieces/mookup/desktop (1).webp
new file mode 100644
index 0000000..f257a1d
Binary files /dev/null and b/public/images/sites/avopieces/mookup/desktop (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/laptop (1).webp b/public/images/sites/avopieces/mookup/laptop (1).webp
new file mode 100644
index 0000000..1a78b07
Binary files /dev/null and b/public/images/sites/avopieces/mookup/laptop (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/mobile-black (1).webp b/public/images/sites/avopieces/mookup/mobile-black (1).webp
new file mode 100644
index 0000000..867d2ae
Binary files /dev/null and b/public/images/sites/avopieces/mookup/mobile-black (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/mobile-white (1).webp b/public/images/sites/avopieces/mookup/mobile-white (1).webp
new file mode 100644
index 0000000..dec013f
Binary files /dev/null and b/public/images/sites/avopieces/mookup/mobile-white (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/tablet-black (1).webp b/public/images/sites/avopieces/mookup/tablet-black (1).webp
new file mode 100644
index 0000000..800c0c4
Binary files /dev/null and b/public/images/sites/avopieces/mookup/tablet-black (1).webp differ
diff --git a/public/images/sites/avopieces/mookup/tablet-white (1).webp b/public/images/sites/avopieces/mookup/tablet-white (1).webp
new file mode 100644
index 0000000..5ef302a
Binary files /dev/null and b/public/images/sites/avopieces/mookup/tablet-white (1).webp differ
diff --git a/public/images/sites/avopieces/pc.webp b/public/images/sites/avopieces/pc.webp
new file mode 100644
index 0000000..cbae2da
Binary files /dev/null and b/public/images/sites/avopieces/pc.webp differ
diff --git a/public/images/sites/avopieces/tablette.webp b/public/images/sites/avopieces/tablette.webp
new file mode 100644
index 0000000..2bd5da3
Binary files /dev/null and b/public/images/sites/avopieces/tablette.webp differ
diff --git a/public/images/sites/avopieces/tel.webp b/public/images/sites/avopieces/tel.webp
new file mode 100644
index 0000000..4215160
Binary files /dev/null and b/public/images/sites/avopieces/tel.webp differ
diff --git a/public/images/sites/etsidemain/mookup/3-devices-black.webp b/public/images/sites/etsidemain/mookup/3-devices-black.webp
new file mode 100644
index 0000000..df7ac60
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/3-devices-black.webp differ
diff --git a/public/images/sites/etsidemain/mookup/3-devices-white.webp b/public/images/sites/etsidemain/mookup/3-devices-white.webp
new file mode 100644
index 0000000..c07535f
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/3-devices-white.webp differ
diff --git a/public/images/sites/etsidemain/mookup/desktop.webp b/public/images/sites/etsidemain/mookup/desktop.webp
new file mode 100644
index 0000000..30321c6
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/desktop.webp differ
diff --git a/public/images/sites/etsidemain/mookup/laptop.webp b/public/images/sites/etsidemain/mookup/laptop.webp
new file mode 100644
index 0000000..a9d7173
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/laptop.webp differ
diff --git a/public/images/sites/etsidemain/mookup/mobile-black.webp b/public/images/sites/etsidemain/mookup/mobile-black.webp
new file mode 100644
index 0000000..7e8f23c
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/mobile-black.webp differ
diff --git a/public/images/sites/etsidemain/mookup/mobile-white.webp b/public/images/sites/etsidemain/mookup/mobile-white.webp
new file mode 100644
index 0000000..5673dc6
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/mobile-white.webp differ
diff --git a/public/images/sites/etsidemain/mookup/tablet-black.webp b/public/images/sites/etsidemain/mookup/tablet-black.webp
new file mode 100644
index 0000000..1fdfea8
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/tablet-black.webp differ
diff --git a/public/images/sites/etsidemain/mookup/tablet-white.webp b/public/images/sites/etsidemain/mookup/tablet-white.webp
new file mode 100644
index 0000000..b04ba77
Binary files /dev/null and b/public/images/sites/etsidemain/mookup/tablet-white.webp differ
diff --git a/public/images/sites/etsidemain/pc.webp b/public/images/sites/etsidemain/pc.webp
new file mode 100644
index 0000000..2c9d557
Binary files /dev/null and b/public/images/sites/etsidemain/pc.webp differ
diff --git a/public/images/sites/etsidemain/tablette.webp b/public/images/sites/etsidemain/tablette.webp
new file mode 100644
index 0000000..4d4c7e0
Binary files /dev/null and b/public/images/sites/etsidemain/tablette.webp differ
diff --git a/public/images/sites/etsidemain/tel.webp b/public/images/sites/etsidemain/tel.webp
new file mode 100644
index 0000000..8d02015
Binary files /dev/null and b/public/images/sites/etsidemain/tel.webp differ
diff --git a/scripts/cleanup-old-images.js b/scripts/cleanup-old-images.js
new file mode 100644
index 0000000..3ad1066
--- /dev/null
+++ b/scripts/cleanup-old-images.js
@@ -0,0 +1,50 @@
+import { unlink } from 'fs/promises';
+import { readdir, stat } from 'fs/promises';
+import { join, extname } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const publicDir = join(__dirname, '..', 'public', 'images');
+
+async function getAllImageFiles(dir) {
+ const files = [];
+ const items = await readdir(dir);
+
+ for (const item of items) {
+ const fullPath = join(dir, item);
+ const itemStat = await stat(fullPath);
+
+ if (itemStat.isDirectory()) {
+ files.push(...(await getAllImageFiles(fullPath)));
+ } else if (['.png', '.jpg', '.jpeg'].includes(extname(item).toLowerCase())) {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+async function main() {
+ console.log('🗑️ Suppression des anciennes images PNG/JPG...\n');
+
+ const imageFiles = await getAllImageFiles(publicDir);
+ console.log(`📊 ${imageFiles.length} images à supprimer\n`);
+
+ let deletedCount = 0;
+ for (const file of imageFiles) {
+ try {
+ await unlink(file);
+ console.log(`✅ Supprimé: ${file}`);
+ deletedCount++;
+ } catch (error) {
+ console.error(`❌ Erreur avec ${file}:`, error.message);
+ }
+ }
+
+ console.log(`\n✨ ${deletedCount} image(s) supprimée(s)!`);
+}
+
+main().catch(console.error);
diff --git a/scripts/optimize-images.js b/scripts/optimize-images.js
new file mode 100644
index 0000000..289f0a5
--- /dev/null
+++ b/scripts/optimize-images.js
@@ -0,0 +1,88 @@
+import sharp from 'sharp';
+import { readdir, stat } from 'fs/promises';
+import { join, extname, dirname, basename } from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const publicDir = join(__dirname, '..', 'public', 'images');
+
+async function getAllImageFiles(dir) {
+ const files = [];
+ const items = await readdir(dir);
+
+ for (const item of items) {
+ const fullPath = join(dir, item);
+ const itemStat = await stat(fullPath);
+
+ if (itemStat.isDirectory()) {
+ files.push(...(await getAllImageFiles(fullPath)));
+ } else if (['.png', '.jpg', '.jpeg'].includes(extname(item).toLowerCase())) {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+async function optimizeImage(filePath) {
+ const ext = extname(filePath).toLowerCase();
+ const dir = dirname(filePath);
+ const name = basename(filePath, ext);
+ const webpPath = join(dir, `${name}.webp`);
+
+ try {
+ const stats = await stat(filePath);
+ const originalSize = stats.size;
+
+ // Convertir en WebP avec optimisation
+ await sharp(filePath)
+ .webp({
+ quality: 85,
+ effort: 6
+ })
+ .toFile(webpPath);
+
+ const newStats = await stat(webpPath);
+ const newSize = newStats.size;
+ const reduction = ((originalSize - newSize) / originalSize * 100).toFixed(2);
+
+ console.log(`✅ ${basename(filePath)}`);
+ console.log(` Original: ${(originalSize / 1024).toFixed(2)} KB`);
+ console.log(` WebP: ${(newSize / 1024).toFixed(2)} KB`);
+ console.log(` Réduction: ${reduction}%\n`);
+
+ return { originalSize, newSize };
+ } catch (error) {
+ console.error(`❌ Erreur avec ${filePath}:`, error.message);
+ return { originalSize: 0, newSize: 0 };
+ }
+}
+
+async function main() {
+ console.log('🖼️ Optimisation des images...\n');
+
+ const imageFiles = await getAllImageFiles(publicDir);
+ console.log(`📊 ${imageFiles.length} images trouvées\n`);
+
+ let totalOriginal = 0;
+ let totalNew = 0;
+
+ for (const file of imageFiles) {
+ const { originalSize, newSize } = await optimizeImage(file);
+ totalOriginal += originalSize;
+ totalNew += newSize;
+ }
+
+ const totalReduction = ((totalOriginal - totalNew) / totalOriginal * 100).toFixed(2);
+
+ console.log('=' .repeat(50));
+ console.log(`📊 RÉSUMÉ TOTAL:`);
+ console.log(` Taille originale: ${(totalOriginal / 1024 / 1024).toFixed(2)} MB`);
+ console.log(` Taille optimisée: ${(totalNew / 1024 / 1024).toFixed(2)} MB`);
+ console.log(` Économie totale: ${totalReduction}%`);
+ console.log(` ${(totalOriginal - totalNew) / 1024 / 1024} MB économisés! 🎉`);
+}
+
+main().catch(console.error);
diff --git a/scripts/update-image-references.js b/scripts/update-image-references.js
new file mode 100644
index 0000000..0d7790e
--- /dev/null
+++ b/scripts/update-image-references.js
@@ -0,0 +1,44 @@
+import { readFile, writeFile } from 'fs/promises';
+import { join } from 'path';
+
+const filesToUpdate = [
+ 'src/components/sections/ProjectsSection.tsx',
+ 'src/data/projects.ts'
+];
+
+async function updateFile(filePath) {
+ try {
+ let content = await readFile(filePath, 'utf-8');
+ const originalContent = content;
+
+ // Remplacer .png, .jpg, .jpeg par .webp
+ content = content.replace(/\.(png|jpg|jpeg)"/g, '.webp"');
+ content = content.replace(/\.(png|jpg|jpeg)'/g, ".webp'");
+
+ if (content !== originalContent) {
+ await writeFile(filePath, content, 'utf-8');
+ console.log(`✅ ${filePath} mis à jour`);
+ return true;
+ } else {
+ console.log(`⏭️ ${filePath} - aucun changement nécessaire`);
+ return false;
+ }
+ } catch (error) {
+ console.error(`❌ Erreur avec ${filePath}:`, error.message);
+ return false;
+ }
+}
+
+async function main() {
+ console.log('🔄 Mise à jour des références d\'images...\n');
+
+ let updatedCount = 0;
+ for (const file of filesToUpdate) {
+ const updated = await updateFile(file);
+ if (updated) updatedCount++;
+ }
+
+ console.log(`\n✨ ${updatedCount} fichier(s) mis à jour!`);
+}
+
+main().catch(console.error);
diff --git a/src/components/sections/ProjectsSection.tsx b/src/components/sections/ProjectsSection.tsx
index 8ca466f..cd3c923 100644
--- a/src/components/sections/ProjectsSection.tsx
+++ b/src/components/sections/ProjectsSection.tsx
@@ -11,37 +11,37 @@ export const ProjectsSection = () => {
{
key: "avopieces",
icon: ,
- image: "/images/sites/avopieces/mookup/3-devices-white (1).png",
+ image: "/images/sites/avopieces/mookup/3-devices-white (1).webp",
},
{
key: "nas",
icon: ,
- image: "/images/projects/homemade_nas.png",
+ image: "/images/projects/homemade_nas.webp",
},
{
key: "transcendence",
icon: ,
- image: "/images/projects/pong.png",
+ image: "/images/projects/pong.webp",
},
{
key: "cloud",
icon: ,
- image: "/images/projects/cloud_1.png",
+ image: "/images/projects/cloud_1.webp",
},
{
key: "minishell",
icon: ,
- image: "/images/projects/minishell.png",
+ image: "/images/projects/minishell.webp",
},
{
key: "etsidemain",
icon: ,
- image: "/images/sites/etsidemain/mookup/3-devices-white.png",
+ image: "/images/sites/etsidemain/mookup/3-devices-white.webp",
},
{
key: "cube3d",
icon: ,
- image: "/images/projects/cub3d.png",
+ image: "/images/projects/cub3d.webp",
},
];
diff --git a/src/data/projects.ts b/src/data/projects.ts
index c01046a..2ea2d19 100644
--- a/src/data/projects.ts
+++ b/src/data/projects.ts
@@ -59,8 +59,8 @@ export const projectsData: Record = {
fr: "Architecture microservices avec Docker Compose, reverse proxy Traefik automatisant les certificats Let's Encrypt, système de backup automatique avec rsync, et monitoring avec Prometheus.",
en: "Microservices architecture with Docker Compose, Traefik reverse proxy automating Let's Encrypt certificates, automatic backup system with rsync, and monitoring with Prometheus.",
},
- mainImage: "/images/projects/homemade_nas.png",
- images: ["/images/projects/homemade_nas.png"],
+ mainImage: "/images/projects/homemade_nas.webp",
+ images: ["/images/projects/homemade_nas.webp"],
technologies: [
{ name: "OpenMediaVault", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/debian/debian-original.svg" },
{ name: "Docker", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/docker/docker-original.svg" },
@@ -113,8 +113,8 @@ export const projectsData: Record = {
fr: "Architecture MVC avec NestJS, WebSocket rooms pour les matchs, système de queuing Redis pour le matchmaking, JWT pour l'authentification, et Canvas HTML5 pour le rendu du jeu.",
en: "MVC architecture with NestJS, WebSocket rooms for matches, Redis queuing system for matchmaking, JWT for authentication, and HTML5 Canvas for game rendering.",
},
- mainImage: "/images/projects/pong.png",
- images: ["/images/projects/pong.png"],
+ mainImage: "/images/projects/pong.webp",
+ images: ["/images/projects/pong.webp"],
technologies: [
{ name: "React", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/react/react-original.svg" },
{ name: "TypeScript", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/typescript/typescript-original.svg" },
@@ -167,8 +167,8 @@ export const projectsData: Record = {
fr: "Docker Compose pour l'orchestration, Ansible playbooks pour l'automatisation, volumes Docker pour la persistance, et réseau bridge personnalisé pour l'isolation.",
en: "Docker Compose for orchestration, Ansible playbooks for automation, Docker volumes for persistence, and custom bridge network for isolation.",
},
- mainImage: "/images/projects/cloud_1.png",
- images: ["/images/projects/cloud_1.png"],
+ mainImage: "/images/projects/cloud_1.webp",
+ images: ["/images/projects/cloud_1.webp"],
technologies: [
{ name: "Docker", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/docker/docker-original.svg" },
{ name: "Ansible", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/ansible/ansible-original.svg" },
@@ -220,8 +220,8 @@ export const projectsData: Record = {
fr: "Tokenizer/Lexer pour le parsing, AST pour représenter les commandes, gestion des descripteurs de fichiers pour les redirections, et table de hash pour les variables d'environnement.",
en: "Tokenizer/Lexer for parsing, AST to represent commands, file descriptor management for redirections, and hash table for environment variables.",
},
- mainImage: "/images/projects/minishell.png",
- images: ["/images/projects/minishell.png"],
+ mainImage: "/images/projects/minishell.webp",
+ images: ["/images/projects/minishell.webp"],
technologies: [
{ name: "C", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/c/c-original.svg" },
{ name: "Linux", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/linux/linux-original.svg" },
@@ -272,8 +272,8 @@ export const projectsData: Record = {
fr: "Algorithme DDA pour le raycasting, lookup tables pour les calculs trigonométriques, buffer d'image pour le double buffering, et grille 2D pour la détection de collisions.",
en: "DDA algorithm for raycasting, lookup tables for trigonometric calculations, image buffer for double buffering, and 2D grid for collision detection.",
},
- mainImage: "/images/projects/cub3d.png",
- images: ["/images/projects/cub3d.png"],
+ mainImage: "/images/projects/cub3d.webp",
+ images: ["/images/projects/cub3d.webp"],
technologies: [
{ name: "C", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/c/c-original.svg" },
{ name: "MiniLibX", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/c/c-original.svg" },
@@ -324,16 +324,16 @@ export const projectsData: Record = {
fr: "HTML5/CSS3 sémantique, animations avec transitions CSS et Intersection Observer, balises meta optimisées, structure de données JSON-LD, et optimisation des assets.",
en: "Semantic HTML5/CSS3, animations with CSS transitions and Intersection Observer, optimized meta tags, JSON-LD data structure, and asset optimization.",
},
- mainImage: "/images/sites/etsidemain/mookup/3-devices-white.png",
+ mainImage: "/images/sites/etsidemain/mookup/3-devices-white.webp",
images: [
- "/images/sites/etsidemain/mookup/3-devices-white.png",
- "/images/sites/etsidemain/mookup/desktop.png",
- "/images/sites/etsidemain/mookup/laptop.png",
- "/images/sites/etsidemain/mookup/tablet-white.png",
- "/images/sites/etsidemain/mookup/mobile-white.png",
- "/images/sites/etsidemain/pc.png",
- "/images/sites/etsidemain/tablette.png",
- "/images/sites/etsidemain/tel.png"
+ "/images/sites/etsidemain/mookup/3-devices-white.webp",
+ "/images/sites/etsidemain/mookup/desktop.webp",
+ "/images/sites/etsidemain/mookup/laptop.webp",
+ "/images/sites/etsidemain/mookup/tablet-white.webp",
+ "/images/sites/etsidemain/mookup/mobile-white.webp",
+ "/images/sites/etsidemain/pc.webp",
+ "/images/sites/etsidemain/tablette.webp",
+ "/images/sites/etsidemain/tel.webp"
],
technologies: [
{ name: "HTML5", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/html5/html5-original.svg" },
@@ -386,16 +386,16 @@ export const projectsData: Record = {
fr: "Architecture MERN stack, RAG (Retrieval Augmented Generation) pour le chatbot, chiffrement des données, système de roles et permissions, API RESTful, et design system cohérent.",
en: "MERN stack architecture, RAG (Retrieval Augmented Generation) for chatbot, data encryption, roles and permissions system, RESTful API, and consistent design system.",
},
- mainImage: "/images/sites/avopieces/mookup/3-devices-white (1).png",
+ mainImage: "/images/sites/avopieces/mookup/3-devices-white (1).webp",
images: [
- "/images/sites/avopieces/mookup/3-devices-white (1).png",
- "/images/sites/avopieces/mookup/desktop (1).png",
- "/images/sites/avopieces/mookup/laptop (1).png",
- "/images/sites/avopieces/mookup/tablet-white (1).png",
- "/images/sites/avopieces/mookup/mobile-white (1).png",
- "/images/sites/avopieces/pc.png",
- "/images/sites/avopieces/tablette.png",
- "/images/sites/avopieces/tel.png"
+ "/images/sites/avopieces/mookup/3-devices-white (1).webp",
+ "/images/sites/avopieces/mookup/desktop (1).webp",
+ "/images/sites/avopieces/mookup/laptop (1).webp",
+ "/images/sites/avopieces/mookup/tablet-white (1).webp",
+ "/images/sites/avopieces/mookup/mobile-white (1).webp",
+ "/images/sites/avopieces/pc.webp",
+ "/images/sites/avopieces/tablette.webp",
+ "/images/sites/avopieces/tel.webp"
],
technologies: [
{ name: "React", iconUrl: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/react/react-original.svg" },