chriswu25 commited on
Commit
1322628
·
verified ·
1 Parent(s): 815307f

Upload 5 files

Browse files
Files changed (5) hide show
  1. src/App.tsx +120 -0
  2. src/index.css +3 -0
  3. src/main.tsx +10 -0
  4. src/types.ts +24 -0
  5. 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" />