@@ -4,7 +4,6 @@ import { useState, type ChangeEvent, useEffect } from "react";
44export default function ImageSizeCompressor ( ) {
55 const [ images , setImages ] = useState < File [ ] > ( [ ] ) ;
66 const [ quality , setQuality ] = useState ( 0.8 ) ;
7- const [ previews , setPreviews ] = useState < string [ ] > ( [ ] ) ;
87 const [ compressedPreview , setCompressedPreview ] = useState < string | null > (
98 null ,
109 ) ;
@@ -20,112 +19,76 @@ export default function ImageSizeCompressor() {
2019 return `${ parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) } ${ sizes [ i ] } ` ;
2120 }
2221
23- function handleImageUpload ( e : ChangeEvent < HTMLInputElement > ) {
24- if ( ! e . target . files ) return ;
22+ async function compressImage ( image : File , quality : number ) : Promise < File > {
23+ return new Promise ( ( resolve , reject ) => {
24+ const img = new Image ( ) ;
25+ const imageUrl = URL . createObjectURL ( image ) ;
26+
27+ img . onload = ( ) => {
28+ const canvas = document . createElement ( "canvas" ) ;
29+ let width = img . width ;
30+ let height = img . height ;
31+ const maxDimension = 1920 ;
32+
33+ if ( width > maxDimension || height > maxDimension ) {
34+ if ( width > height ) {
35+ height = ( height / width ) * maxDimension ;
36+ width = maxDimension ;
37+ } else {
38+ width = ( width / height ) * maxDimension ;
39+ height = maxDimension ;
40+ }
41+ }
2542
26- const newFiles = Array . from ( e . target . files ) ;
27- setImages ( ( prev ) => [ ...prev , ...newFiles ] ) ;
43+ canvas . width = width ;
44+ canvas . height = height ;
45+
46+ const ctx = canvas . getContext ( "2d" ) ;
47+ if ( ! ctx ) return reject ( new Error ( "Could not get canvas context" ) ) ;
48+
49+ ctx . drawImage ( img , 0 , 0 , width , height ) ;
2850
29- newFiles . forEach ( ( file ) => {
30- const reader = new FileReader ( ) ;
31- reader . onloadend = ( ) => {
32- setPreviews ( ( prev ) => [ ...prev , reader . result as string ] ) ;
51+ canvas . toBlob (
52+ ( blob ) => {
53+ if ( ! blob ) return reject ( new Error ( "Could not create blob" ) ) ;
54+
55+ const compressedFile = new File ( [ blob ] , image . name , {
56+ type : "image/jpeg" ,
57+ lastModified : Date . now ( ) ,
58+ } ) ;
59+
60+ resolve ( compressedFile ) ;
61+ } ,
62+ "image/jpeg" ,
63+ quality ,
64+ ) ;
3365 } ;
34- reader . readAsDataURL ( file ) ;
35- } ) ;
3666
37- if ( newFiles [ 0 ] ) {
38- setOriginalSize ( formatFileSize ( newFiles [ 0 ] . size ) ) ;
39- }
67+ img . onerror = ( ) => reject ( new Error ( "Could not load image" ) ) ;
68+ img . src = imageUrl ;
69+ } ) ;
4070 }
4171
42- function removeImage ( index : number ) {
43- setImages ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) ) ;
44- setPreviews ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) ) ;
45- if ( index === 0 ) {
46- setCompressedPreview ( null ) ;
47- setOriginalSize ( "" ) ;
48- setCompressedSize ( "" ) ;
49- }
72+ function handleImageUpload ( e : ChangeEvent < HTMLInputElement > ) {
73+ if ( ! e . target . files ) return ;
74+
75+ const newFiles = Array . from ( e . target . files ) ;
76+ setImages ( ( prev ) => [ ...prev , ...newFiles ] ) ;
5077 }
5178
5279 useEffect ( ( ) => {
53- let isMounted = true ;
80+ if ( images [ 0 ] === undefined ) return ;
81+ setOriginalSize ( formatFileSize ( images [ 0 ] . size ) ) ;
5482
5583 async function generateCompressedPreview ( ) {
56- if ( images [ 0 ] === undefined ) {
57- setCompressedPreview ( null ) ;
58- setCompressedSize ( "" ) ;
59- return ;
60- }
61-
84+ if ( images [ 0 ] === undefined ) return ;
85+ setIsCompressing ( true ) ;
6286 try {
63- // Just a loading state
64- setIsCompressing ( true ) ;
65-
66- // Using the canvas
67-
68- // Create an image element to load the original image
69- const img = new Image ( ) ;
70- const imageUrl = URL . createObjectURL ( images [ 0 ] ) ;
71-
72- img . onload = ( ) => {
73- // Create canvas
74- const canvas = document . createElement ( "canvas" ) ;
75-
76- // Calculate new dimensions while maintaining aspect ratio
77- let width = img . width ;
78- let height = img . height ;
79- const maxDimension = 1920 ;
80-
81- if ( width > maxDimension || height > maxDimension ) {
82- if ( width > height ) {
83- height = ( height / width ) * maxDimension ;
84- width = maxDimension ;
85- } else {
86- width = ( width / height ) * maxDimension ;
87- height = maxDimension ;
88- }
89- }
90-
91- canvas . width = width ;
92- canvas . height = height ;
93-
94- // Draw image on canvas
95- const ctx = canvas . getContext ( "2d" ) ;
96- if ( ! ctx ) return ;
97-
98- ctx . drawImage ( img , 0 , 0 , width , height ) ;
99-
100- // Convert to blob with quality setting
101- canvas . toBlob (
102- ( blob ) => {
103- if ( ! blob || ! isMounted ) return ;
104-
105- const compressedFile = new File ( [ blob ] , images [ 0 ] ! . name , {
106- type : "image/jpeg" ,
107- lastModified : Date . now ( ) ,
108- } ) ;
109-
110- setCompressedSize ( formatFileSize ( compressedFile . size ) ) ;
111- setCompressedPreview ( URL . createObjectURL ( compressedFile ) ) ;
112- } ,
113- "image/jpeg" ,
114- quality ,
115- ) ;
116- } ;
117-
118- img . src = imageUrl ;
119- } catch ( error ) {
120- console . error ( "Error generating preview:" , error ) ;
121- if ( isMounted ) {
122- setCompressedPreview ( null ) ;
123- setCompressedSize ( "" ) ;
124- }
87+ const compressedFile = await compressImage ( images [ 0 ] , quality ) ;
88+ setCompressedPreview ( URL . createObjectURL ( compressedFile ) ) ;
89+ setCompressedSize ( formatFileSize ( compressedFile . size ) ) ;
12590 } finally {
126- if ( isMounted ) {
127- setIsCompressing ( false ) ;
128- }
91+ setIsCompressing ( false ) ;
12992 }
13093 }
13194
@@ -134,64 +97,24 @@ export default function ImageSizeCompressor() {
13497 } , 300 ) ;
13598
13699 return ( ) => {
137- isMounted = false ;
138100 clearTimeout ( debounceTimeout ) ;
139101 } ;
140- } , [ quality , images ] ) ;
102+ } , [ images , quality ] ) ;
103+
104+ function removeImage ( index : number ) {
105+ setImages ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) ) ;
106+ if ( index === 0 ) {
107+ setCompressedPreview ( null ) ;
108+ setOriginalSize ( "" ) ;
109+ setCompressedSize ( "" ) ;
110+ }
111+ }
141112
142113 async function handleCompress ( ) {
143114 try {
115+ setIsCompressing ( true ) ;
144116 const compressedFiles = await Promise . all (
145- images . map ( async ( image ) => {
146- return new Promise < File > ( ( resolve , reject ) => {
147- const img = new Image ( ) ;
148- const imageUrl = URL . createObjectURL ( image ) ;
149-
150- img . onload = ( ) => {
151- const canvas = document . createElement ( "canvas" ) ;
152- let width = img . width ;
153- let height = img . height ;
154- const maxDimension = 1920 ;
155-
156- if ( width > maxDimension || height > maxDimension ) {
157- if ( width > height ) {
158- height = ( height / width ) * maxDimension ;
159- width = maxDimension ;
160- } else {
161- width = ( width / height ) * maxDimension ;
162- height = maxDimension ;
163- }
164- }
165-
166- canvas . width = width ;
167- canvas . height = height ;
168-
169- const ctx = canvas . getContext ( "2d" ) ;
170- if ( ! ctx )
171- return reject ( new Error ( "Could not get canvas context" ) ) ;
172-
173- ctx . drawImage ( img , 0 , 0 , width , height ) ;
174-
175- canvas . toBlob (
176- ( blob ) => {
177- if ( ! blob ) return reject ( new Error ( "Could not create blob" ) ) ;
178-
179- const compressedFile = new File ( [ blob ] , image . name , {
180- type : "image/jpeg" ,
181- lastModified : Date . now ( ) ,
182- } ) ;
183-
184- resolve ( compressedFile ) ;
185- } ,
186- "image/jpeg" ,
187- quality ,
188- ) ;
189- } ;
190-
191- img . onerror = ( ) => reject ( new Error ( "Could not load image" ) ) ;
192- img . src = imageUrl ;
193- } ) ;
194- } ) ,
117+ images . map ( ( image ) => compressImage ( image , quality ) ) ,
195118 ) ;
196119
197120 compressedFiles . forEach ( ( file , index ) => {
@@ -203,6 +126,8 @@ export default function ImageSizeCompressor() {
203126 } ) ;
204127 } catch ( error ) {
205128 console . error ( "Error compressing images:" , error ) ;
129+ } finally {
130+ setIsCompressing ( false ) ;
206131 }
207132 }
208133
@@ -212,7 +137,6 @@ export default function ImageSizeCompressor() {
212137
213138 function onCancel ( ) {
214139 setImages ( [ ] ) ;
215- setPreviews ( [ ] ) ;
216140 setCompressedPreview ( null ) ;
217141 setOriginalSize ( "" ) ;
218142 setCompressedSize ( "" ) ;
@@ -241,10 +165,10 @@ export default function ImageSizeCompressor() {
241165 return (
242166 < div className = "flex flex-col items-center justify-center gap-4 p-4 text-2xl" >
243167 < div className = "flex flex-wrap justify-center gap-4" >
244- { previews . map ( ( preview , index ) => (
168+ { images . map ( ( image , index ) => (
245169 < div key = { index } className = "relative" >
246170 < img
247- src = { preview }
171+ src = { URL . createObjectURL ( image ) }
248172 alt = { `Preview ${ index + 1 } ` }
249173 className = "h-32 w-32 rounded-lg object-cover"
250174 />
@@ -268,6 +192,7 @@ export default function ImageSizeCompressor() {
268192 value = { quality }
269193 onChange = { onChangeQuality }
270194 className = "w-full"
195+ disabled = { isCompressing }
271196 />
272197 </ div >
273198
@@ -276,7 +201,7 @@ export default function ImageSizeCompressor() {
276201 < div className = "flex flex-col items-center gap-2" >
277202 < span className = "text-sm font-medium" > Original</ span >
278203 < img
279- src = { previews [ 0 ] }
204+ src = { images [ 0 ] ? URL . createObjectURL ( images [ 0 ] ) : "" }
280205 alt = "Original preview"
281206 className = "h-64 w-64 rounded-lg object-cover"
282207 />
@@ -286,7 +211,10 @@ export default function ImageSizeCompressor() {
286211 < span className = "text-sm font-medium" > Compressed Preview</ span >
287212 < div className = "relative h-64 w-64" >
288213 < img
289- src = { compressedPreview ?? previews [ 0 ] }
214+ src = {
215+ compressedPreview ??
216+ ( images [ 0 ] ? URL . createObjectURL ( images [ 0 ] ) : "" )
217+ }
290218 alt = "Compressed preview"
291219 className = "h-64 w-64 rounded-lg object-cover"
292220 />
@@ -304,13 +232,23 @@ export default function ImageSizeCompressor() {
304232 < div className = "flex gap-2" >
305233 < button
306234 onClick = { handleCompress }
307- className = "rounded-lg bg-green-700 px-4 py-2 text-sm font-semibold text-white shadow-md transition-colors duration-200 hover:bg-green-800 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75"
235+ disabled = { isCompressing }
236+ className = { `rounded-lg px-4 py-2 text-sm font-semibold text-white shadow-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75 ${
237+ isCompressing
238+ ? "cursor-not-allowed bg-gray-500"
239+ : "bg-green-700 hover:bg-green-800"
240+ } `}
308241 >
309- Download Compressed Images
242+ { isCompressing ? "Compressing..." : " Download Compressed Images" }
310243 </ button >
311244 < button
312245 onClick = { onCancel }
313- className = "rounded-md bg-red-700 px-3 py-1 text-sm font-medium text-white transition-colors hover:bg-red-800"
246+ disabled = { isCompressing }
247+ className = { `rounded-md px-3 py-1 text-sm font-medium text-white transition-colors ${
248+ isCompressing
249+ ? "cursor-not-allowed bg-gray-500"
250+ : "bg-red-700 hover:bg-red-800"
251+ } `}
314252 >
315253 Cancel
316254 </ button >
0 commit comments