Upload 5 files
Browse files- src/App.tsx +120 -0
- src/index.css +3 -0
- src/main.tsx +10 -0
- src/types.ts +24 -0
- src/vite-env.d.ts +1 -0
src/App.tsx
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useCallback } from 'react';
|
2 |
+
import { Image, Trash2 } from 'lucide-react';
|
3 |
+
import { CompressionOptions } from './components/CompressionOptions';
|
4 |
+
import { DropZone } from './components/DropZone';
|
5 |
+
import { ImageList } from './components/ImageList';
|
6 |
+
import { DownloadAll } from './components/DownloadAll';
|
7 |
+
import { useImageQueue } from './hooks/useImageQueue';
|
8 |
+
import { DEFAULT_QUALITY_SETTINGS } from './utils/formatDefaults';
|
9 |
+
import type { ImageFile, OutputType, CompressionOptions as CompressionOptionsType } from './types';
|
10 |
+
|
11 |
+
export function App() {
|
12 |
+
const [images, setImages] = useState<ImageFile[]>([]);
|
13 |
+
const [outputType, setOutputType] = useState<OutputType>('webp');
|
14 |
+
const [options, setOptions] = useState<CompressionOptionsType>({
|
15 |
+
quality: DEFAULT_QUALITY_SETTINGS.webp,
|
16 |
+
});
|
17 |
+
|
18 |
+
const { addToQueue } = useImageQueue(options, outputType, setImages);
|
19 |
+
|
20 |
+
const handleOutputTypeChange = useCallback((type: OutputType) => {
|
21 |
+
setOutputType(type);
|
22 |
+
if (type !== 'png') {
|
23 |
+
setOptions({ quality: DEFAULT_QUALITY_SETTINGS[type] });
|
24 |
+
}
|
25 |
+
}, []);
|
26 |
+
|
27 |
+
const handleFilesDrop = useCallback((newImages: ImageFile[]) => {
|
28 |
+
// First add all images to state
|
29 |
+
setImages((prev) => [...prev, ...newImages]);
|
30 |
+
|
31 |
+
// Use requestAnimationFrame to wait for render to complete
|
32 |
+
requestAnimationFrame(() => {
|
33 |
+
// Then add to queue after UI has updated
|
34 |
+
newImages.forEach(image => addToQueue(image.id));
|
35 |
+
});
|
36 |
+
}, [addToQueue]);
|
37 |
+
|
38 |
+
const handleRemoveImage = useCallback((id: string) => {
|
39 |
+
setImages((prev) => {
|
40 |
+
const image = prev.find(img => img.id === id);
|
41 |
+
if (image?.preview) {
|
42 |
+
URL.revokeObjectURL(image.preview);
|
43 |
+
}
|
44 |
+
return prev.filter(img => img.id !== id);
|
45 |
+
});
|
46 |
+
}, []);
|
47 |
+
|
48 |
+
const handleClearAll = useCallback(() => {
|
49 |
+
images.forEach(image => {
|
50 |
+
if (image.preview) {
|
51 |
+
URL.revokeObjectURL(image.preview);
|
52 |
+
}
|
53 |
+
});
|
54 |
+
setImages([]);
|
55 |
+
}, [images]);
|
56 |
+
|
57 |
+
const handleDownloadAll = useCallback(async () => {
|
58 |
+
const completedImages = images.filter((img) => img.status === "complete");
|
59 |
+
|
60 |
+
for (const image of completedImages) {
|
61 |
+
if (image.blob && image.outputType) {
|
62 |
+
const link = document.createElement("a");
|
63 |
+
link.href = URL.createObjectURL(image.blob);
|
64 |
+
link.download = `${image.file.name.split(".")[0]}.${image.outputType}`;
|
65 |
+
link.click();
|
66 |
+
URL.revokeObjectURL(link.href);
|
67 |
+
}
|
68 |
+
|
69 |
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
70 |
+
}
|
71 |
+
}, [images]);
|
72 |
+
|
73 |
+
const completedImages = images.filter(img => img.status === 'complete').length;
|
74 |
+
|
75 |
+
return (
|
76 |
+
<div className="min-h-screen bg-gray-50">
|
77 |
+
<div className="max-w-4xl mx-auto px-4 py-12">
|
78 |
+
<div className="text-center mb-8">
|
79 |
+
<div className="flex items-center justify-center gap-2 mb-4">
|
80 |
+
<Image className="w-8 h-8 text-blue-500" />
|
81 |
+
<h1 className="text-3xl font-bold text-gray-900">Squish</h1>
|
82 |
+
</div>
|
83 |
+
<p className="text-gray-600">
|
84 |
+
Compress and convert your images to AVIF, JPEG, JPEG XL, PNG, or WebP
|
85 |
+
</p>
|
86 |
+
</div>
|
87 |
+
|
88 |
+
<div className="space-y-6">
|
89 |
+
<CompressionOptions
|
90 |
+
options={options}
|
91 |
+
outputType={outputType}
|
92 |
+
onOptionsChange={setOptions}
|
93 |
+
onOutputTypeChange={handleOutputTypeChange}
|
94 |
+
/>
|
95 |
+
|
96 |
+
<DropZone onFilesDrop={handleFilesDrop} />
|
97 |
+
|
98 |
+
{completedImages > 0 && (
|
99 |
+
<DownloadAll onDownloadAll={handleDownloadAll} count={completedImages} />
|
100 |
+
)}
|
101 |
+
|
102 |
+
<ImageList
|
103 |
+
images={images}
|
104 |
+
onRemove={handleRemoveImage}
|
105 |
+
/>
|
106 |
+
|
107 |
+
{images.length > 0 && (
|
108 |
+
<button
|
109 |
+
onClick={handleClearAll}
|
110 |
+
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
111 |
+
>
|
112 |
+
<Trash2 className="w-5 h-5" />
|
113 |
+
Clear All
|
114 |
+
</button>
|
115 |
+
)}
|
116 |
+
</div>
|
117 |
+
</div>
|
118 |
+
</div>
|
119 |
+
);
|
120 |
+
}
|
src/index.css
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
src/main.tsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { StrictMode } from 'react';
|
2 |
+
import { createRoot } from 'react-dom/client';
|
3 |
+
import { App } from './App';
|
4 |
+
import './index.css';
|
5 |
+
|
6 |
+
createRoot(document.getElementById('root')!).render(
|
7 |
+
<StrictMode>
|
8 |
+
<App />
|
9 |
+
</StrictMode>
|
10 |
+
);
|
src/types.ts
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface ImageFile {
|
2 |
+
id: string;
|
3 |
+
file: File;
|
4 |
+
preview?: string;
|
5 |
+
status: 'pending' | 'queued' | 'processing' | 'complete' | 'error';
|
6 |
+
error?: string;
|
7 |
+
originalSize: number;
|
8 |
+
compressedSize?: number;
|
9 |
+
outputType?: OutputType;
|
10 |
+
blob?: Blob;
|
11 |
+
}
|
12 |
+
|
13 |
+
export type OutputType = 'avif' | 'jpeg' | 'jxl' | 'png' | 'webp';
|
14 |
+
|
15 |
+
export interface FormatQualitySettings {
|
16 |
+
avif: number;
|
17 |
+
jpeg: number;
|
18 |
+
jxl: number;
|
19 |
+
webp: number;
|
20 |
+
}
|
21 |
+
|
22 |
+
export interface CompressionOptions {
|
23 |
+
quality: number;
|
24 |
+
}
|
src/vite-env.d.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
/// <reference types="vite/client" />
|