perf: create optimized thumbnails for project cards - 96% size reduction (615KB -> 24KB) for card preview images
@ -11,6 +11,7 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"optimize-images": "node scripts/optimize-images.js",
|
"optimize-images": "node scripts/optimize-images.js",
|
||||||
|
"create-thumbnails": "node scripts/create-thumbnails.js",
|
||||||
"analyze": "node scripts/analyze-bundle.js"
|
"analyze": "node scripts/analyze-bundle.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
BIN
public/images/projects/cloud_1_thumb.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/projects/cub3d_thumb.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/projects/homemade_nas_thumb.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/images/projects/minishell_thumb.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/projects/pong_thumb.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/images/sites/etsidemain/mookup/3-devices-white_thumb.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
83
scripts/create-thumbnails.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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');
|
||||||
|
|
||||||
|
// Liste des images utilisées dans les cartes projet (miniatures)
|
||||||
|
const thumbnailImages = [
|
||||||
|
'projects/homemade_nas.webp',
|
||||||
|
'projects/pong.webp',
|
||||||
|
'projects/cloud_1.webp',
|
||||||
|
'projects/minishell.webp',
|
||||||
|
'projects/cub3d.webp',
|
||||||
|
'sites/avopieces/mookup/3-devices-white (1).webp',
|
||||||
|
'sites/etsidemain/mookup/3-devices-white.webp'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function createThumbnail(filePath, size = 128) {
|
||||||
|
const ext = extname(filePath);
|
||||||
|
const dir = dirname(filePath);
|
||||||
|
const name = basename(filePath, ext);
|
||||||
|
const thumbnailPath = join(dir, `${name}_thumb${ext}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await stat(filePath);
|
||||||
|
const originalSize = stats.size;
|
||||||
|
|
||||||
|
// Créer une miniature carrée de 128x128
|
||||||
|
await sharp(filePath)
|
||||||
|
.resize(size, size, {
|
||||||
|
fit: 'cover',
|
||||||
|
position: 'center'
|
||||||
|
})
|
||||||
|
.webp({
|
||||||
|
quality: 85,
|
||||||
|
effort: 6
|
||||||
|
})
|
||||||
|
.toFile(thumbnailPath);
|
||||||
|
|
||||||
|
const newStats = await stat(thumbnailPath);
|
||||||
|
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(` Miniature: ${(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('🖼️ Création des miniatures pour les cartes projet...\n');
|
||||||
|
|
||||||
|
let totalOriginal = 0;
|
||||||
|
let totalNew = 0;
|
||||||
|
|
||||||
|
for (const imagePath of thumbnailImages) {
|
||||||
|
const fullPath = join(publicDir, imagePath);
|
||||||
|
const { originalSize, newSize } = await createThumbnail(fullPath);
|
||||||
|
totalOriginal += originalSize;
|
||||||
|
totalNew += newSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalReduction = ((totalOriginal - totalNew) / totalOriginal * 100).toFixed(2);
|
||||||
|
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
console.log(`📊 RÉSUMÉ DES MINIATURES:`);
|
||||||
|
console.log(` Taille originale: ${(totalOriginal / 1024).toFixed(2)} KB`);
|
||||||
|
console.log(` Taille miniatures: ${(totalNew / 1024).toFixed(2)} KB`);
|
||||||
|
console.log(` Économie totale: ${totalReduction}%`);
|
||||||
|
console.log(` ${(totalOriginal - totalNew) / 1024} KB économisés! 🎉`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@ -28,22 +28,22 @@ export const ProjectCard = ({ title, description, icon, image, technologies, del
|
|||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay }}
|
transition={{ duration: 0.5, delay }}
|
||||||
whileHover={{ y: -5 }}
|
whileHover={{ y: -5, transition: { duration: 0.2 } }}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={projectId ? "cursor-pointer" : ""}
|
className={projectId ? "cursor-pointer" : ""}
|
||||||
>
|
>
|
||||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur relative overflow-hidden group">
|
<Card className="h-full hover:shadow-lg transition-all duration-200 border-border/50 bg-card/50 backdrop-blur relative overflow-hidden group">
|
||||||
{/* Indicateur cliquable en bas à droite */}
|
{/* Indicateur cliquable en bas à droite */}
|
||||||
{projectId && (
|
{projectId && (
|
||||||
<div className="absolute bottom-4 right-4 w-8 h-8 rounded-full bg-primary/10 group-hover:bg-primary/20 flex items-center justify-center transition-all duration-300 group-hover:scale-110">
|
<div className="absolute bottom-4 right-4 w-8 h-8 rounded-full bg-primary/10 group-hover:bg-primary/20 flex items-center justify-center transition-all duration-200 group-hover:scale-110">
|
||||||
<ArrowRight className="w-4 h-4 text-primary group-hover:translate-x-0.5 transition-transform duration-300" />
|
<ArrowRight className="w-4 h-4 text-primary group-hover:translate-x-0.5 transition-transform duration-200" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{image && (
|
{image && (
|
||||||
<div className="absolute top-4 right-4 w-16 h-16 rounded-lg overflow-hidden border-2 border-background/20 shadow-lg">
|
<div className="absolute top-4 right-4 w-16 h-16 rounded-lg overflow-hidden border-2 border-background/20 shadow-lg">
|
||||||
<img
|
<img
|
||||||
src={image}
|
src={image.replace('.webp', '_thumb.webp')}
|
||||||
alt={`${title} preview`}
|
alt={`${title} preview`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||