Skip to content

Commit 3c64496

Browse files
authored
Merge pull request abue-ammar#10 from abue-ammar/ui-migration
feat: add file type validation and error handling in image compressor…
2 parents 836d887 + 9213eba commit 3c64496

5 files changed

Lines changed: 102 additions & 3 deletions

File tree

package-lock.json

Lines changed: 22 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
"compressorjs": "^1.2.1",
1919
"jszip": "^3.10.1",
2020
"lucide-react": "^0.525.0",
21+
"next-themes": "^0.4.6",
2122
"react": "^19.1.0",
2223
"react-dom": "^19.1.0",
2324
"react-photo-view": "^1.2.4",
25+
"sonner": "^2.0.6",
2426
"tailwind-merge": "^3.3.1"
2527
},
2628
"devDependencies": {

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Footer from "./components/footer";
22
import Header from "./components/header";
33
import ImageCompressor from "./components/image-compressor";
44
import { ThemeProvider } from "./components/theme-provider";
5+
import { Toaster } from "./components/ui/sonner";
56

67
function App() {
78
return (
@@ -10,6 +11,7 @@ function App() {
1011
<Header />
1112
<main className="flex-1">
1213
<ImageCompressor />
14+
<Toaster />
1315
</main>
1416
<Footer />
1517
</div>

src/components/image-compressor.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import JSZip from "jszip";
33
import { Download, ImageIcon, Inbox, RefreshCcw } from "lucide-react";
44
import { useEffect, useRef, useState } from "react";
55
import { PhotoProvider } from "react-photo-view";
6+
import { toast } from "sonner";
67

78
import ImagePreviewCard from "./image-preview-card";
89
import ImageQualitySlider from "./image-quality-slider";
@@ -31,6 +32,45 @@ const ImageCompressor = () => {
3132
const [compressProgress, setCompressProgress] = useState<number>(0);
3233
const dropAreaRef = useRef<HTMLLabelElement>(null);
3334
const compressedImagesRef = useRef<HTMLDivElement>(null);
35+
36+
// Allowed image formats
37+
const allowedFormats = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
38+
39+
// Validate file type
40+
const validateFileType = (file: File): boolean => {
41+
return allowedFormats.includes(file.type.toLowerCase());
42+
};
43+
44+
// Filter valid files and show error for invalid ones
45+
const filterValidFiles = (files: FileList | File[]): File[] => {
46+
const filesArray = Array.from(files);
47+
const validFiles: File[] = [];
48+
const invalidFiles: File[] = [];
49+
50+
filesArray.forEach((file) => {
51+
if (validateFileType(file)) {
52+
validFiles.push(file);
53+
} else {
54+
invalidFiles.push(file);
55+
}
56+
});
57+
58+
// Show error toast for invalid files
59+
if (invalidFiles.length > 0) {
60+
const invalidFileNames = invalidFiles.map((file) => file.name).join(", ");
61+
toast.error(
62+
`Invalid file! Please upload only JPG, JPEG, PNG, or WEBP files.`,
63+
{
64+
description: invalidFileNames,
65+
duration: 5000,
66+
position: "top-right",
67+
}
68+
);
69+
}
70+
71+
return validFiles;
72+
};
73+
3474
const onImageQualityChange = async (
3575
event: React.ChangeEvent<HTMLInputElement>
3676
) => {
@@ -40,9 +80,13 @@ const ImageCompressor = () => {
4080
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
4181
e.preventDefault();
4282
if (e.target.files) {
83+
const validFiles = filterValidFiles(e.target.files);
84+
if (validFiles.length === 0) {
85+
return; // No valid files to process
86+
}
4387
setCompressedImages([]);
4488
setCompressProgress(0);
45-
setFilelist(e.target.files);
89+
setFilelist(validFiles);
4690
}
4791
};
4892

@@ -144,9 +188,15 @@ const ImageCompressor = () => {
144188
e.preventDefault();
145189
e.stopPropagation();
146190
setIsDragActive(false);
191+
192+
const validFiles = filterValidFiles(e.dataTransfer.files);
193+
if (validFiles.length === 0) {
194+
return; // No valid files to process
195+
}
196+
147197
setCompressedImages([]);
148198
setCompressProgress(0);
149-
setFilelist(e.dataTransfer.files);
199+
setFilelist(validFiles);
150200
};
151201

152202
const handleDownload = () => {
@@ -212,7 +262,7 @@ const ImageCompressor = () => {
212262
<input
213263
multiple
214264
type="file"
215-
accept="image/*"
265+
accept="image/jpeg,image/jpg,image/png,image/webp"
216266
onChange={handleImageUpload}
217267
style={{ display: "none" }}
218268
id="file-input"

src/components/ui/sonner.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useTheme } from "next-themes"
2+
import { Toaster as Sonner, type ToasterProps } from "sonner"
3+
4+
const Toaster = ({ ...props }: ToasterProps) => {
5+
const { theme = "system" } = useTheme()
6+
7+
return (
8+
<Sonner
9+
theme={theme as ToasterProps["theme"]}
10+
className="toaster group"
11+
style={
12+
{
13+
"--normal-bg": "var(--popover)",
14+
"--normal-text": "var(--popover-foreground)",
15+
"--normal-border": "var(--border)",
16+
} as React.CSSProperties
17+
}
18+
{...props}
19+
/>
20+
)
21+
}
22+
23+
export { Toaster }

0 commit comments

Comments
 (0)