feat: add automatic image compression before upload

Added client-side image compression to improve upload speed and reduce storage.

New Features:
- Created imageCompression utility (src/utils/imageCompression.ts)
  * Automatically compresses images larger than 1MB
  * Resizes to max 1920x1080px while preserving aspect ratio
  * Uses 80% JPEG quality by default
  * Preserves PNG transparency when present
  * Falls back to 60% quality if still over 2MB

Mobile (NoteDetailPage.tsx):
- Integrated compression before upload
- Shows compression stats to user (e.g., "5.2 MB → 1.1 MB")
- Better error messages with specific details

Desktop (Index.tsx):
- Same compression integration as mobile
- Consistent user experience across platforms

Benefits:
✅ Users can upload large photos from cameras/phones
✅ Faster uploads (smaller files)
✅ Reduced server storage requirements
✅ Better mobile experience (photos often 5-10MB)
✅ Transparent to user (happens automatically)

Technical Details:
- Uses HTML5 Canvas API for processing
- Client-side compression (no server load)
- Maintains image quality while reducing size
- Handles JPEG, PNG, WebP, and GIF formats
- Smart transparency detection for PNGs
This commit is contained in:
Claude committed 2025-11-27 13:14:37 +00:00
1 parent 185fe92b6a
commit cc635f786f
3 files changed
+198 -22

No files matched your search

+22 -11
View File
@@ -31,6 +31,7 @@ import { showError, showSuccess } from "@/utils/toast";
import { ConfirmModal } from "@/components/modals/ConfirmModal";
import { InputModal } from "@/components/modals/InputModal";
import { AddUserModal } from "@/components/modals/AddUserModal";
import { compressImage, formatFileSize } from "@/utils/imageCompression";
// ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS =====
@@ -1339,18 +1340,28 @@ const Index = () => {
className="hidden"
onChange={async (e) => {
if (e.target.files && e.target.files[0] && openNote?.id) {
const file = e.target.files[0];
// Vérifier la taille du fichier (5MB max)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
showError("L'image est trop grande (max 5MB)");
e.target.value = '';
return;
}
const originalFile = e.target.files[0];
const originalSize = formatFileSize(originalFile.size);
try {
const image = await NotesService.uploadImage(openNote.id, file);
// Compresser l'image avant l'upload
const compressedFile = await compressImage(originalFile);
const compressedSize = formatFileSize(compressedFile.size);
// Afficher un message si l'image a été compressée
if (compressedFile.size < originalFile.size) {
showSuccess(`Image compressée: ${originalSize} → ${compressedSize}`);
}
// Vérifier la taille finale (5MB max après compression)
const maxSize = 5 * 1024 * 1024; // 5MB
if (compressedFile.size > maxSize) {
showError("L'image est trop grande même après compression (max 5MB)");
e.target.value = '';
return;
}
const image = await NotesService.uploadImage(openNote.id, compressedFile);
if (image) {
const updatedImages = [...(openNote.images || []), image];
setOpenNote({ ...openNote, images: updatedImages });
@@ -1360,7 +1371,7 @@ const Index = () => {
showError("Erreur lors de l'upload de l'image");
}
} catch (error) {
showError("Erreur lors de l'upload de l'image");
showError(error instanceof Error ? error.message : "Erreur lors de l'upload de l'image");
}
e.target.value = '';
}
+22 -11
View File
@@ -27,6 +27,7 @@ import {
} from "@/components/ui/dropdown-menu";
import NotesService, { Note } from "@/services/NotesService";
import { showError, showSuccess } from "@/utils/toast";
import { compressImage, formatFileSize } from "@/utils/imageCompression";
export default function NoteDetailPage() {
const { id } = useParams();
@@ -119,18 +120,28 @@ export default function NoteDetailPage() {
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files[0] || !note?.id) return;
const file = e.target.files[0];
// Vérifier la taille du fichier (5MB max)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
showError("L'image est trop grande (max 5MB)");
e.target.value = '';
return;
}
const originalFile = e.target.files[0];
const originalSize = formatFileSize(originalFile.size);
try {
const image = await NotesService.uploadImage(note.id, file);
// Compresser l'image avant l'upload
const compressedFile = await compressImage(originalFile);
const compressedSize = formatFileSize(compressedFile.size);
// Afficher un message si l'image a été compressée
if (compressedFile.size < originalFile.size) {
showSuccess(`Image compressée: ${originalSize} → ${compressedSize}`);
}
// Vérifier la taille finale (5MB max après compression)
const maxSize = 5 * 1024 * 1024; // 5MB
if (compressedFile.size > maxSize) {
showError("L'image est trop grande même après compression (max 5MB)");
e.target.value = '';
return;
}
const image = await NotesService.uploadImage(note.id, compressedFile);
if (image) {
const updatedImages = [...(note.images || []), image];
setNote({ ...note, images: updatedImages });
@@ -139,7 +150,7 @@ export default function NoteDetailPage() {
showError("Erreur lors de l'upload de l'image");
}
} catch (error) {
showError("Erreur lors de l'upload");
showError(error instanceof Error ? error.message : "Erreur lors de l'upload");
}
e.target.value = '';
};