Skip to content

Commit bcaa91c

Browse files
authored
Merge pull request abue-ammar#16 from abue-ammar/ui-migration
refactor: enhance animations and layout for improved user experience
2 parents 647dfda + d781bac commit bcaa91c

6 files changed

Lines changed: 134 additions & 66 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Toaster } from "./components/ui/sonner";
77
function App() {
88
return (
99
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
10-
<div className="relative flex min-h-screen flex-col">
10+
<div className="flex min-h-screen flex-col">
1111
<Header />
1212
<main className="flex-1">
1313
<ImageCompressor />

src/components/footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const Footer = () => {
22
return (
3-
<footer className="bg-background text-foreground container mx-auto flex items-center justify-center px-4 py-2 text-center">
3+
<footer className="bg-background text-foreground animate-fadeIn animate-delay-300 container mx-auto flex items-center justify-center px-4 py-2 text-center">
44
<span className="inline-flex gap-x-1">
55
Built with{" "}
66
<span>

src/components/image-compressor.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,13 @@ const ImageCompressor = () => {
241241
return (
242242
<div className="container mx-auto px-4">
243243
<Intro />
244-
<ImageQualitySlider
245-
value={value}
246-
onImageQualityChange={onImageQualityChange}
247-
/>
248-
<div className="">
244+
<div className="animate-fadeIn animate-delay-150">
245+
<ImageQualitySlider
246+
value={value}
247+
onImageQualityChange={onImageQualityChange}
248+
/>
249+
</div>
250+
<div className="animate-fadeIn animate-delay-200">
249251
<label
250252
ref={dropAreaRef}
251253
className={`relative flex flex-col items-center overflow-hidden rounded-xl border-2 border-dashed p-2 transition-all duration-200 ease-in-out ${
@@ -288,7 +290,7 @@ const ImageCompressor = () => {
288290
</label>
289291

290292
{compressedImages?.length > 0 && filelist?.length > 0 && (
291-
<div className="mt-4 flex justify-end gap-x-4">
293+
<div className="animate-fadeInFast mt-4 flex justify-end gap-x-4">
292294
<Button variant={"default"} onClick={handleDownload}>
293295
<Download />
294296
Download All (ZIP)
@@ -317,18 +319,23 @@ const ImageCompressor = () => {
317319
{/* <h2 className="text-lg font-bold">Compressed Images</h2> */}
318320
{compressedImages?.length > 0 ? (
319321
<PhotoProvider>
320-
<div className="grid grid-cols-1 gap-4 py-4 md:grid-cols-2 lg:grid-cols-3">
322+
<div className="grid grid-cols-1 gap-4 py-4 will-change-transform md:grid-cols-2 lg:grid-cols-3">
321323
{compressedImages.map((image, i) => (
322-
<ImagePreviewCard
323-
key={i}
324-
onSingleFileDownload={onSingleFileDownload}
325-
{...image}
326-
/>
324+
<div
325+
key={`${image.fileName}-${i}`}
326+
className="animate-fadeInFast"
327+
style={{ animationDelay: `${Math.min(i * 50, 300)}ms` }}
328+
>
329+
<ImagePreviewCard
330+
onSingleFileDownload={onSingleFileDownload}
331+
{...image}
332+
/>
333+
</div>
327334
))}
328335
</div>
329336
</PhotoProvider>
330337
) : (
331-
<div className="text-muted-foreground flex flex-col items-center justify-center py-8 text-center">
338+
<div className="text-muted-foreground animate-fadeIn flex flex-col items-center justify-center py-8 text-center">
332339
<Inbox className="size-14" strokeWidth={1.5} />
333340
<h3 className="mb-1 text-base font-medium">
334341
No Compressed Images
Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,66 @@
11
import { formatBytes } from "@/utils/utils";
22
import { DownloadIcon, Eye, MoveRight } from "lucide-react";
3+
import { memo } from "react";
34
import { PhotoView } from "react-photo-view";
45
import "react-photo-view/dist/react-photo-view.css";
56
import { Button } from "./ui/button";
67

7-
const ImagePreviewCard = ({
8-
onSingleFileDownload,
9-
...props
10-
}: {
11-
onSingleFileDownload: (file: string) => void;
12-
content: string;
13-
fileName: string;
14-
originalImageSize: number;
15-
compressedImageSize: number;
16-
compressionPercentage: string;
17-
}) => {
18-
return (
19-
<div className="bg-background flex items-center justify-between gap-2 rounded-lg border p-2 pe-3">
20-
<div className="flex items-center gap-3 overflow-hidden">
21-
<div className="bg-accent relative aspect-square shrink-0 cursor-pointer rounded-[6px]">
22-
<PhotoView src={props?.content}>
23-
<div>
24-
<img
25-
src={props?.content}
26-
alt={props?.fileName}
27-
className="size-12 rounded-[6px] object-cover"
28-
/>
29-
<div className="absolute inset-0 flex items-center justify-center rounded-[6px] bg-black/25">
30-
<Eye className="text-[#fafafa]" />
8+
const ImagePreviewCard = memo(
9+
({
10+
onSingleFileDownload,
11+
...props
12+
}: {
13+
onSingleFileDownload: (file: string) => void;
14+
content: string;
15+
fileName: string;
16+
originalImageSize: number;
17+
compressedImageSize: number;
18+
compressionPercentage: string;
19+
}) => {
20+
return (
21+
<div className="bg-background flex items-center justify-between gap-2 rounded-lg border p-2 pe-3 will-change-transform">
22+
<div className="flex items-center gap-3 overflow-hidden">
23+
<div className="bg-accent relative aspect-square shrink-0 cursor-pointer rounded-[6px]">
24+
<PhotoView src={props?.content}>
25+
<div>
26+
<img
27+
src={props?.content}
28+
alt={props?.fileName}
29+
className="size-12 rounded-[6px] object-cover"
30+
/>
31+
<div className="absolute inset-0 flex items-center justify-center rounded-[6px] bg-black/25">
32+
<Eye className="text-[#fafafa]" />
33+
</div>
3134
</div>
32-
</div>
33-
</PhotoView>
34-
</div>
35-
<div className="flex min-w-0 flex-col">
36-
<p className="truncate text-sm font-medium">{props?.fileName}</p>
37-
<span className="text-muted-foreground flex items-center text-xs">
38-
<span className="text-destructive">
39-
{formatBytes(props?.originalImageSize)}
40-
</span>
41-
<MoveRight className="mx-1 h-4 w-4" />
42-
<span className="text-success">
43-
{formatBytes(props?.compressedImageSize)}{" "}
44-
<span className="inline-flex">
45-
({props?.compressionPercentage}%)
35+
</PhotoView>
36+
</div>
37+
<div className="flex min-w-0 flex-col">
38+
<p className="truncate text-sm font-medium">{props?.fileName}</p>
39+
<span className="text-muted-foreground flex items-center text-xs">
40+
<span className="text-destructive">
41+
{formatBytes(props?.originalImageSize)}
42+
</span>
43+
<MoveRight className="mx-1 h-4 w-4" />
44+
<span className="text-success">
45+
{formatBytes(props?.compressedImageSize)}{" "}
46+
<span className="inline-flex">
47+
({props?.compressionPercentage}%)
48+
</span>
4649
</span>
4750
</span>
48-
</span>
51+
</div>
4952
</div>
53+
<Button
54+
size="icon"
55+
variant="ghost"
56+
className="text-muted-foreground/80 hover:text-foreground -me-2 size-8 hover:bg-transparent"
57+
onClick={() => onSingleFileDownload(props?.content)}
58+
>
59+
<DownloadIcon aria-hidden="true" />
60+
</Button>
5061
</div>
51-
<Button
52-
size="icon"
53-
variant="ghost"
54-
className="text-muted-foreground/80 hover:text-foreground -me-2 size-8 hover:bg-transparent"
55-
onClick={() => onSingleFileDownload(props?.content)}
56-
>
57-
<DownloadIcon aria-hidden="true" />
58-
</Button>
59-
</div>
60-
);
61-
};
62+
);
63+
}
64+
);
6265

6366
export default ImagePreviewCard;

src/components/intro.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const Intro = () => {
22
return (
3-
<section>
3+
<section className="animate-fadeIn animate-delay-100">
44
<div className="container mx-auto px-4 py-10 md:px-6 lg:py-16 2xl:max-w-[1400px]">
55
{/* Announcement Banner */}
66
<div className="flex justify-center">

src/index.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,64 @@
44

55
@custom-variant dark (&:is(.dark *));
66

7+
/* Fade-in animations */
8+
@keyframes fadeIn {
9+
from {
10+
opacity: 0;
11+
transform: translateY(10px);
12+
}
13+
to {
14+
opacity: 1;
15+
transform: translateY(0);
16+
}
17+
}
18+
19+
@keyframes fadeInFast {
20+
from {
21+
opacity: 0;
22+
transform: translateY(5px);
23+
}
24+
to {
25+
opacity: 1;
26+
transform: translateY(0);
27+
}
28+
}
29+
30+
.animate-fadeIn {
31+
animation: fadeIn 0.4s ease-out forwards;
32+
opacity: 0;
33+
}
34+
35+
.animate-fadeInFast {
36+
animation: fadeInFast 0.3s ease-out forwards;
37+
opacity: 0;
38+
}
39+
40+
/* Performance optimizations */
41+
.will-change-transform {
42+
will-change: transform, opacity;
43+
}
44+
45+
.animate-delay-100 {
46+
animation-delay: 100ms;
47+
}
48+
49+
.animate-delay-150 {
50+
animation-delay: 150ms;
51+
}
52+
53+
.animate-delay-200 {
54+
animation-delay: 200ms;
55+
}
56+
57+
.animate-delay-250 {
58+
animation-delay: 250ms;
59+
}
60+
61+
.animate-delay-300 {
62+
animation-delay: 300ms;
63+
}
64+
765
html {
866
scroll-behavior: smooth;
967
font-variation-settings: normal;

0 commit comments

Comments
 (0)