[dyad] Redesign image converter layout - wrote 1 file(s)
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Upload, Download, Image as ImageIcon, X, Trash2 } from "lucide-react";
|
import { Upload, Download, Image as ImageIcon, X, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -39,7 +39,6 @@ export function ImageConverter() {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clean up object URLs on component unmount
|
|
||||||
return () => {
|
return () => {
|
||||||
previewUrls.forEach((url) => URL.revokeObjectURL(url));
|
previewUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
};
|
};
|
||||||
@@ -78,7 +77,9 @@ export function ImageConverter() {
|
|||||||
];
|
];
|
||||||
const newFilenames = [
|
const newFilenames = [
|
||||||
...filenames,
|
...filenames,
|
||||||
...imageFiles.map(file => file.name.substring(0, file.name.lastIndexOf('.')))
|
...imageFiles.map((file) =>
|
||||||
|
file.name.substring(0, file.name.lastIndexOf("."))
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
setImages(newImages);
|
setImages(newImages);
|
||||||
@@ -123,7 +124,9 @@ export function ImageConverter() {
|
|||||||
setFilenames(newFilenames);
|
setFilenames(newFilenames);
|
||||||
|
|
||||||
if (selectedImageIndex === indexToRemove) {
|
if (selectedImageIndex === indexToRemove) {
|
||||||
setSelectedImageIndex(newImages.length > 0 ? Math.max(0, indexToRemove - 1) : null);
|
setSelectedImageIndex(
|
||||||
|
newImages.length > 0 ? Math.max(0, indexToRemove - 1) : null
|
||||||
|
);
|
||||||
} else if (selectedImageIndex !== null && selectedImageIndex > indexToRemove) {
|
} else if (selectedImageIndex !== null && selectedImageIndex > indexToRemove) {
|
||||||
setSelectedImageIndex(selectedImageIndex - 1);
|
setSelectedImageIndex(selectedImageIndex - 1);
|
||||||
}
|
}
|
||||||
@@ -138,10 +141,12 @@ export function ImageConverter() {
|
|||||||
toast.info("All images cleared.");
|
toast.info("All images cleared.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilenameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleFilenameChange = (
|
||||||
if (selectedImageIndex === null) return;
|
e: ChangeEvent<HTMLInputElement>,
|
||||||
|
index: number
|
||||||
|
) => {
|
||||||
const newFilenames = [...filenames];
|
const newFilenames = [...filenames];
|
||||||
newFilenames[selectedImageIndex] = e.target.value;
|
newFilenames[index] = e.target.value;
|
||||||
setFilenames(newFilenames);
|
setFilenames(newFilenames);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,8 +155,11 @@ export function ImageConverter() {
|
|||||||
toast.error("Please select an image and set dimensions.");
|
toast.error("Please select an image and set dimensions.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!filenames[selectedImageIndex] || filenames[selectedImageIndex].trim() === "") {
|
if (
|
||||||
toast.error("Please enter a filename.");
|
!filenames[selectedImageIndex] ||
|
||||||
|
filenames[selectedImageIndex].trim() === ""
|
||||||
|
) {
|
||||||
|
toast.error("Please enter a filename for the selected image.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,8 +202,8 @@ export function ImageConverter() {
|
|||||||
const isImageSelected = selectedImageIndex !== null;
|
const isImageSelected = selectedImageIndex !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 w-full">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 w-full">
|
||||||
<div className="flex flex-col gap-8">
|
<div className="lg:col-span-1 flex flex-col gap-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Image Settings</CardTitle>
|
<CardTitle>Image Settings</CardTitle>
|
||||||
@@ -225,23 +233,26 @@ export function ImageConverter() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="filename">Filename</Label>
|
|
||||||
<Input
|
|
||||||
id="filename"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter filename"
|
|
||||||
value={isImageSelected ? filenames[selectedImageIndex] : ""}
|
|
||||||
onChange={handleFilenameChange}
|
|
||||||
disabled={!isImageSelected}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between">
|
<CardFooter className="flex justify-between">
|
||||||
<Button variant="outline" onClick={() => fileInputRef.current?.click()}><Upload className="mr-2 h-4 w-4" />Upload Images</Button>
|
<Button variant="outline" onClick={() => fileInputRef.current?.click()}><Upload className="mr-2 h-4 w-4" />Upload</Button>
|
||||||
<Button onClick={handleConvertAndDownload} disabled={!isImageSelected || isConverting}><Download className="mr-2 h-4 w-4" />{isConverting ? "Converting..." : "Convert & Download"}</Button>
|
<Button onClick={handleConvertAndDownload} disabled={!isImageSelected || isConverting}><Download className="mr-2 h-4 w-4" />{isConverting ? "Converting..." : "Download"}</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-2 flex flex-col gap-8">
|
||||||
|
<div className={cn("w-full aspect-video rounded-md border flex items-center justify-center bg-gray-100 dark:bg-gray-800 relative transition-colors", isDraggingOver && "border-primary bg-primary/10")} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}>
|
||||||
|
{isImageSelected && previewUrls[selectedImageIndex] ? (
|
||||||
|
<img src={previewUrls[selectedImageIndex]} alt="Selected image preview" className="max-w-full max-h-full object-contain" />
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full border-2 border-dashed rounded-md flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50" onClick={() => fileInputRef.current?.click()}>
|
||||||
|
<ImageIcon className="w-12 h-12 mb-2" />
|
||||||
|
<span>Click or drag & drop images here</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasImages && (
|
{hasImages && (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -252,35 +263,31 @@ export function ImageConverter() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ScrollArea className="w-full whitespace-nowrap">
|
<ScrollArea className="h-[400px] pr-4">
|
||||||
<div className="flex space-x-4 pb-4">
|
<div className="space-y-4">
|
||||||
{previewUrls.map((url, index) => (
|
{previewUrls.map((url, index) => (
|
||||||
<div key={url} className="relative shrink-0">
|
<div key={url} className={cn("p-4 border rounded-lg flex items-center gap-4 cursor-pointer transition-all", selectedImageIndex === index && "ring-2 ring-primary shadow-md")} onClick={() => setSelectedImageIndex(index)}>
|
||||||
<button onClick={() => setSelectedImageIndex(index)} className={cn("w-24 h-24 rounded-md overflow-hidden border-2 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", selectedImageIndex === index ? "border-primary" : "border-transparent")}>
|
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
||||||
<img src={url} alt={`Preview ${index + 1}`} className="w-full h-full object-cover" />
|
<div className="flex-1 space-y-2 min-w-0">
|
||||||
</button>
|
<p className="text-sm font-medium truncate text-gray-700 dark:text-gray-300" title={images[index].name}>
|
||||||
<Button variant="destructive" size="icon" className="absolute -top-2 -right-2 h-6 w-6 rounded-full" onClick={() => handleRemoveImage(index)}><X className="h-4 w-4" /></Button>
|
Source: {images[index].name}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor={`filename-${index}`} className="text-xs font-semibold">New Filename</Label>
|
||||||
|
<Input id={`filename-${index}`} type="text" placeholder="Enter new filename" value={filenames[index]} onChange={(e) => handleFilenameChange(e, index)} onClick={(e) => e.stopPropagation()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" className="shrink-0 text-gray-500 hover:text-destructive" onClick={(e) => { e.stopPropagation(); handleRemoveImage(index); }}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<ScrollBar orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("w-full aspect-video rounded-md border flex items-center justify-center bg-gray-100 dark:bg-gray-800 relative transition-colors", isDraggingOver && "border-primary bg-primary/10")} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}>
|
|
||||||
{isImageSelected && previewUrls[selectedImageIndex] ? (
|
|
||||||
<img src={previewUrls[selectedImageIndex]} alt="Selected image preview" className="max-w-full max-h-full object-contain" />
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full border-2 border-dashed rounded-md flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50" onClick={() => fileInputRef.current?.click()}>
|
|
||||||
<ImageIcon className="w-12 h-12 mb-2" />
|
|
||||||
<span>Click or drag & drop images here</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user