Skip to content

Commit f0b687a

Browse files
committed
feat: add new features and improve image processing
- Introduced `fast-average-color` for enhanced color extraction. - Added `react-router-dom` for improved routing capabilities. - Enhanced error handling in image/folder APIs with more descriptive messages. - Refactored image processing with `sharp` for better performance and flexibility. - Implemented caching for image responses to optimize loading times. - Added `ColorContext` for managing color states across components. - Replaced deprecated `ParticleBackground` with `AuraBackground` for dynamic visuals. These changes enhance UX, improve performance, and streamline image handling.
1 parent b2b20d7 commit f0b687a

26 files changed

+1980
-716
lines changed

package-lock.json

Lines changed: 102 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"classnames": "^2.5.1",
4949
"dotenv": "^16.4.7",
5050
"express": "^4.19.2",
51+
"fast-average-color": "^9.5.0",
5152
"formidable": "^3.5.1",
5253
"framer-motion": "^11.5.4",
5354
"gl-matrix": "^3.4.3",

src/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from 'react';
21
import Home from './pages/Home.js';
32
import './styles/views.css';
43

src/api/folders.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import express from 'express';
2-
import fs from 'fs';
2+
import { promises as fs } from 'fs';
33
import path from 'path';
44

55
const router = express.Router();
66

7-
router.get('/folders', (req, res) => {
7+
router.get('/folders', async (req, res) => {
88
const mainDir = process.env.MAIN_DIRECTORY || path.join(process.cwd(), 'public', 'images');
99

1010
try {
11-
const folders = fs
12-
.readdirSync(mainDir, { withFileTypes: true })
11+
const dirents = await fs.readdir(mainDir, { withFileTypes: true });
12+
13+
const folders = dirents
1314
.filter(dirent => dirent.isDirectory())
1415
.map(dirent => ({
1516
name: dirent.name,
@@ -19,7 +20,14 @@ router.get('/folders', (req, res) => {
1920
res.json(folders);
2021
} catch (error) {
2122
console.error('Error reading folders:', error);
22-
res.status(500).json({ error: 'Failed to read folders' });
23+
let errorMessage = 'Failed to read folders';
24+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
25+
errorMessage = `Directory not found: ${mainDir}`;
26+
console.error(errorMessage);
27+
res.status(404).json({ error: errorMessage });
28+
} else {
29+
res.status(500).json({ error: errorMessage });
30+
}
2331
}
2432
});
2533

src/api/image.ts

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,100 @@
11
import express from 'express';
22
import fs from 'fs';
33
import path from 'path';
4+
import sharp from 'sharp';
45

56
const router = express.Router();
67

7-
router.get('/image', (req, res) => {
8-
const { folder, file } = req.query;
8+
// Basic Cache (Replace with a more robust solution if needed)
9+
const imageCache = new Map<string, Buffer>();
10+
const CACHE_MAX_SIZE = 100; // Example limit
11+
12+
router.get('/image', async (req, res) => {
13+
const { folder, file, w, h } = req.query;
914
const mainDir = process.env.MAIN_DIRECTORY;
1015

1116
if (!mainDir) {
12-
return res.status(500).json({ error: 'MAIN_DIRECTORY is not set in environment variables' });
17+
return res.status(500).json({ error: 'MAIN_DIRECTORY is not set' });
1318
}
1419

1520
if (typeof folder !== 'string' || typeof file !== 'string') {
16-
return res.status(400).json({ error: 'Invalid folder or file parameter' });
21+
return res.status(400).json({ error: 'Invalid folder or file' });
1722
}
1823

1924
const filePath = path.join(mainDir, folder, file);
2025

21-
if (fs.existsSync(filePath)) {
22-
const imageBuffer = fs.readFileSync(filePath);
23-
const contentType = getContentType(path.extname(file));
24-
res.setHeader('Content-Type', contentType);
25-
res.send(imageBuffer);
26-
} else {
27-
res.status(404).json({ error: 'Image not found' });
26+
// --- Dimension Parsing ---
27+
let targetWidth: number | undefined = parseInt(w as string, 10);
28+
let targetHeight: number | undefined = parseInt(h as string, 10);
29+
if (isNaN(targetWidth)) targetWidth = undefined;
30+
if (isNaN(targetHeight)) targetHeight = undefined;
31+
32+
// Basic validation (adjust limits as needed)
33+
if (targetWidth !== undefined && (targetWidth <= 0 || targetWidth > 4000)) {
34+
return res.status(400).json({ error: 'Invalid width parameter' });
35+
}
36+
if (targetHeight !== undefined && (targetHeight <= 0 || targetHeight > 4000)) {
37+
return res.status(400).json({ error: 'Invalid height parameter' });
38+
}
39+
40+
try {
41+
// Check if file exists before proceeding
42+
if (!fs.existsSync(filePath)) {
43+
return res.status(404).json({ error: 'Image not found' });
44+
}
45+
46+
// --- Serve Original if no dimensions requested ---
47+
if (targetWidth === undefined && targetHeight === undefined) {
48+
const contentType = getContentType(path.extname(file));
49+
res.setHeader('Content-Type', contentType);
50+
// Use stream for potentially large originals
51+
fs.createReadStream(filePath).pipe(res);
52+
return; // Important: stop execution here
53+
}
54+
55+
// --- Handle Resizing ---
56+
const format = req.accepts('image/webp') ? 'webp' : 'jpeg'; // Prefer WebP
57+
const cacheKey = `${filePath}_w${targetWidth || 'auto'}_h${targetHeight || 'auto'}_${format}`;
58+
59+
// Check cache
60+
if (imageCache.has(cacheKey)) {
61+
console.log(`Serving from cache: ${cacheKey}`);
62+
res.setHeader('Content-Type', format === 'webp' ? 'image/webp' : 'image/jpeg');
63+
return res.send(imageCache.get(cacheKey));
64+
}
65+
66+
console.log(`Resizing: ${cacheKey}`);
67+
const transformer = sharp(filePath).resize({
68+
width: targetWidth,
69+
height: targetHeight,
70+
fit: 'inside', // Maintain aspect ratio within bounds
71+
withoutEnlargement: true, // Don't upscale
72+
});
73+
74+
// Set output format
75+
if (format === 'webp') {
76+
transformer.webp({ quality: 80 });
77+
} else {
78+
transformer.jpeg({ quality: 85, progressive: true }); // Progressive JPEG
79+
}
80+
81+
// Get buffer, send, and cache
82+
const outputBuffer = await transformer.toBuffer();
83+
84+
// Add to cache (simple eviction)
85+
if (imageCache.size >= CACHE_MAX_SIZE) {
86+
const firstKey = imageCache.keys().next().value;
87+
if (firstKey) {
88+
imageCache.delete(firstKey);
89+
}
90+
}
91+
imageCache.set(cacheKey, outputBuffer);
92+
93+
res.setHeader('Content-Type', format === 'webp' ? 'image/webp' : 'image/jpeg');
94+
res.send(outputBuffer);
95+
} catch (error) {
96+
console.error(`Error processing image ${filePath}:`, error);
97+
res.status(500).json({ error: 'Error processing image' });
2898
}
2999
});
30100

src/api/images.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import express from 'express';
2-
import fs from 'fs';
2+
import { promises as fs } from 'fs';
33
import path from 'path';
44
import { ImageInfo } from '../types.js';
5-
import { isImageFile, getImageDimensions } from '../utils/imageUtils.js';
5+
import { getImageDimensions, isImageFile } from '../utils/imageUtils.js';
66

77
const router = express.Router();
88

@@ -26,10 +26,15 @@ router.get('/images', async (req, res) => {
2626
res.status(200).json(images);
2727
} catch (error: any) {
2828
console.error('Error in getImages:', error);
29-
if (error.message.includes('Folder not found')) {
29+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
3030
res.status(404).json({ error: `Folder not found: ${folder}` });
31+
} else if (error instanceof Error && error.message.includes('Access denied')) {
32+
res.status(403).json({ error: error.message });
3133
} else {
32-
res.status(500).json({ error: 'Failed to fetch images', details: error.message });
34+
res.status(500).json({
35+
error: 'Failed to fetch images',
36+
details: error instanceof Error ? error.message : String(error),
37+
});
3338
}
3439
}
3540
});
@@ -44,13 +49,18 @@ async function getImages(folder: string): Promise<ImageInfo[]> {
4449
throw new Error('Invalid folder path: Access denied');
4550
}
4651

47-
if (!fs.existsSync(imagesDirectory)) {
48-
throw new Error(`Folder not found: ${folder}`);
52+
let fileNames: string[];
53+
try {
54+
fileNames = await fs.readdir(imagesDirectory);
55+
} catch (error) {
56+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
57+
throw new Error(`Folder not found: ${folder}`);
58+
} else {
59+
console.error(`Error reading directory ${imagesDirectory}:`, error);
60+
throw new Error('Failed to read image directory');
61+
}
4962
}
5063

51-
const fileNames = fs.readdirSync(imagesDirectory);
52-
53-
// First collect the promises with proper typing
5464
const imagePromises = fileNames.filter(isImageFile).map(async fileName => {
5565
try {
5666
const id = fileName.replace(/\.[^/.]+$/, '');
@@ -71,7 +81,6 @@ async function getImages(folder: string): Promise<ImageInfo[]> {
7181
}
7282
});
7383

74-
// Then resolve promises and filter out nulls with type guard
7584
const results = await Promise.all(imagePromises);
7685
const images: ImageInfo[] = results.filter((image): image is ImageInfo => image !== null);
7786

0 commit comments

Comments
 (0)