[dyad] Added tooltips to all settings - wrote 1 file(s)
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Upload, Download, X, Trash2, Check, ArrowRightLeft } from "lucide-react";
|
import { Upload, Download, X, Trash2, Check, ArrowRightLeft, HelpCircle } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
@@ -30,6 +30,12 @@ import {
|
|||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { ObjectPositionControl } from "./object-position-control";
|
import { ObjectPositionControl } from "./object-position-control";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const aspectRatios = [
|
const aspectRatios = [
|
||||||
{ name: "Custom", value: "custom" },
|
{ name: "Custom", value: "custom" },
|
||||||
@@ -359,285 +365,432 @@ export function ImageConverter() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 w-full">
|
<TooltipProvider>
|
||||||
<div className="lg:col-span-1 flex flex-col gap-4 lg:sticky lg:top-8 self-start">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 w-full">
|
||||||
<Accordion type="single" collapsible defaultValue="image-settings" className="w-full space-y-4">
|
<div className="lg:col-span-1 flex flex-col gap-4 lg:sticky lg:top-8 self-start">
|
||||||
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
<Accordion type="single" collapsible defaultValue="image-settings" className="w-full space-y-4">
|
||||||
<AccordionTrigger className="p-6 hover:no-underline">
|
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
||||||
<div className="text-left">
|
<AccordionTrigger className="p-6 hover:no-underline">
|
||||||
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
<div className="text-left">
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
||||||
Adjust resolution and scaling for all images.
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
</p>
|
Adjust resolution and scaling for all images.
|
||||||
</div>
|
</p>
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="px-6 pb-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
|
|
||||||
<Select value={aspectRatio} onValueChange={handleAspectRatioChange}>
|
|
||||||
<SelectTrigger id="aspect-ratio" className="mt-2">
|
|
||||||
<SelectValue placeholder="Select aspect ratio" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{aspectRatios.map((ratio) => (
|
|
||||||
<SelectItem key={ratio.value} value={ratio.value}>
|
|
||||||
{ratio.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-2">
|
</AccordionTrigger>
|
||||||
<div className="space-y-2 flex-1">
|
<AccordionContent className="px-6 pb-6">
|
||||||
<Label htmlFor="width">Width (px)</Label>
|
<div className="space-y-4">
|
||||||
<Input id="width" type="number" placeholder="Original" value={width} onChange={handleWidthChange} />
|
<div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Select value={aspectRatio} onValueChange={handleAspectRatioChange}>
|
||||||
|
<SelectTrigger id="aspect-ratio" className="mt-2">
|
||||||
|
<SelectValue placeholder="Select aspect ratio" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{aspectRatios.map((ratio) => (
|
||||||
|
<SelectItem key={ratio.value} value={ratio.value}>
|
||||||
|
{ratio.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="icon" onClick={handleSwapDimensions} className="shrink-0" aria-label="Swap width and height">
|
<div className="flex items-end gap-2">
|
||||||
<ArrowRightLeft className="h-4 w-4" />
|
<div className="space-y-2 flex-1">
|
||||||
</Button>
|
<Tooltip>
|
||||||
<div className="space-y-2 flex-1">
|
<TooltipTrigger asChild>
|
||||||
<Label htmlFor="height">Height (px)</Label>
|
<Label htmlFor="width">Width (px)</Label>
|
||||||
<Input id="height" type="number" placeholder="Original" value={height} onChange={handleHeightChange} />
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Set the output width in pixels. Leave blank to use the original width.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Input id="width" type="number" placeholder="Original" value={width} onChange={handleWidthChange} />
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon" onClick={handleSwapDimensions} className="shrink-0" aria-label="Swap width and height">
|
||||||
|
<ArrowRightLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Swap the entered width and height values.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="height">Height (px)</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Set the output height in pixels. Leave blank to use the original height.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Input id="height" type="number" placeholder="Original" value={height} onChange={handleHeightChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 pt-2">
|
||||||
|
<Checkbox id="keep-orientation" checked={keepOrientation} onCheckedChange={(checked) => setKeepOrientation(Boolean(checked))} />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="keep-orientation" className="cursor-pointer">Keep original orientation</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Automatically swaps width and height to match the original image's orientation.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 pt-2">
|
|
||||||
<Checkbox id="keep-orientation" checked={keepOrientation} onCheckedChange={(checked) => setKeepOrientation(Boolean(checked))} />
|
|
||||||
<Label htmlFor="keep-orientation" className="cursor-pointer">Keep original orientation</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
<Label htmlFor="scale-mode">Scaling</Label>
|
|
||||||
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
|
|
||||||
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
|
|
||||||
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
|
|
||||||
<SelectItem value="contain">Contain (letterbox)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{scaleMode !== 'fill' && (
|
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<Label>Position</Label>
|
<Tooltip>
|
||||||
<ObjectPositionControl value={objectPosition} onChange={(pos) => setObjectPosition(pos)} />
|
<TooltipTrigger asChild>
|
||||||
</div>
|
<Label htmlFor="scale-mode">Scaling</Label>
|
||||||
)}
|
</TooltipTrigger>
|
||||||
</AccordionContent>
|
<TooltipContent>
|
||||||
</AccordionItem>
|
<p>Determines how the image fits into the new dimensions.</p>
|
||||||
|
</TooltipContent>
|
||||||
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
</Tooltip>
|
||||||
<AccordionTrigger className="p-6 hover:no-underline">
|
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
|
||||||
<div className="text-left">
|
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
||||||
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="px-6 pb-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch id="use-default-base-name" checked={useDefaultBaseName} onCheckedChange={setUseDefaultBaseName} />
|
|
||||||
<Label htmlFor="use-default-base-name">Use default base name</Label>
|
|
||||||
</div>
|
|
||||||
{useDefaultBaseName && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="default-base-name">Default base name</Label>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
id="default-base-name"
|
|
||||||
placeholder="e.g., new-york-trip"
|
|
||||||
value={defaultBaseName}
|
|
||||||
onChange={(e) => setDefaultBaseName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button variant="outline" size="sm" onClick={handleApplyDefaultBaseNameToAll} disabled={!defaultBaseName || !hasImages}>
|
|
||||||
Apply to all
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="prefix">Prefix</Label>
|
|
||||||
<Input id="prefix" placeholder="e.g., travel-" value={prefix} onChange={(e) => setPrefix(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="suffix">Suffix</Label>
|
|
||||||
<Input id="suffix" placeholder="e.g., -edit" value={suffix} onChange={(e) => setSuffix(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 pt-2">
|
|
||||||
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} />
|
|
||||||
<Label htmlFor="use-counter">Add sequential number</Label>
|
|
||||||
</div>
|
|
||||||
{useCounter && (
|
|
||||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="counter-start">Start number</Label>
|
|
||||||
<Input
|
|
||||||
id="counter-start"
|
|
||||||
type="number"
|
|
||||||
value={counterStart}
|
|
||||||
onChange={(e) => setCounterStart(Math.max(0, Number(e.target.value)))}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="counter-digits">Padding digits</Label>
|
|
||||||
<Input
|
|
||||||
id="counter-digits"
|
|
||||||
type="number"
|
|
||||||
value={counterDigits}
|
|
||||||
onChange={(e) => setCounterDigits(Math.max(1, Number(e.target.value)))}
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
|
||||||
<AccordionTrigger className="p-6 hover:no-underline">
|
|
||||||
<div className="text-left">
|
|
||||||
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="px-6 pb-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="format">Format</Label>
|
|
||||||
<Select value={format} onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)}>
|
|
||||||
<SelectTrigger id="format"><SelectValue placeholder="Select format" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="png">PNG</SelectItem>
|
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
|
||||||
<SelectItem value="jpeg">JPEG</SelectItem>
|
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
|
||||||
<SelectItem value="webp">WEBP</SelectItem>
|
<SelectItem value="contain">Contain (letterbox)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
{scaleMode !== 'fill' && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="mt-4 space-y-2">
|
||||||
<Label htmlFor="quality">Quality</Label>
|
<Tooltip>
|
||||||
<span className="text-sm text-muted-foreground">{quality}%</span>
|
<TooltipTrigger asChild>
|
||||||
|
<Label>Position</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<ObjectPositionControl value={objectPosition} onChange={(pos) => setObjectPosition(pos)} />
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
)}
|
||||||
id="quality"
|
</AccordionContent>
|
||||||
min={0}
|
</AccordionItem>
|
||||||
max={100}
|
|
||||||
step={1}
|
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
||||||
value={[quality]}
|
<AccordionTrigger className="p-6 hover:no-underline">
|
||||||
onValueChange={(value) => setQuality(value[0])}
|
<div className="text-left">
|
||||||
disabled={format === 'png'}
|
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
|
||||||
/>
|
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
|
||||||
{format === 'png' && (
|
</div>
|
||||||
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-6 pb-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch id="use-default-base-name" checked={useDefaultBaseName} onCheckedChange={setUseDefaultBaseName} />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="use-default-base-name">Use default base name</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>When enabled, all newly uploaded images will use the specified default base name.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{useDefaultBaseName && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="default-base-name">Default base name</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="default-base-name"
|
||||||
|
placeholder="e.g., new-york-trip"
|
||||||
|
value={defaultBaseName}
|
||||||
|
onChange={(e) => setDefaultBaseName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleApplyDefaultBaseNameToAll} disabled={!defaultBaseName || !hasImages}>
|
||||||
|
Apply to all
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Apply this base name to all currently uploaded images.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="prefix">Prefix</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Add text to the beginning of every filename.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Input id="prefix" placeholder="e.g., travel-" value={prefix} onChange={(e) => setPrefix(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="suffix">Suffix</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Add text to the end of every filename (before the number).</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Input id="suffix" placeholder="e.g., -edit" value={suffix} onChange={(e) => setSuffix(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 pt-2">
|
||||||
|
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="use-counter">Add sequential number</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Append a numbered sequence to each filename.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{useCounter && (
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="counter-start">Start number</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>The first number to use in the sequence.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Input
|
||||||
|
id="counter-start"
|
||||||
|
type="number"
|
||||||
|
value={counterStart}
|
||||||
|
onChange={(e) => setCounterStart(Math.max(0, Number(e.target.value)))}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label htmlFor="counter-digits">Padding digits</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Input
|
||||||
|
id="counter-digits"
|
||||||
|
type="number"
|
||||||
|
value={counterDigits}
|
||||||
|
onChange={(e) => setCounterDigits(Math.max(1, Number(e.target.value)))}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionContent>
|
||||||
</AccordionContent>
|
</AccordionItem>
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
<Button
|
|
||||||
onClick={handleApplySettings}
|
|
||||||
disabled={!hasImages}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Check className="mr-2 h-4 w-4" />
|
|
||||||
Apply Settings
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lg:col-span-2 flex flex-col gap-4">
|
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
||||||
<Card>
|
<AccordionTrigger className="p-6 hover:no-underline">
|
||||||
<CardContent className="pt-6">
|
<div className="text-left">
|
||||||
<div className="space-y-4">
|
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
|
||||||
<h3 className="text-lg font-medium">Upload Images</h3>
|
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
|
|
||||||
isDraggingOver ? "border-primary bg-accent" : "border-input"
|
|
||||||
)}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
|
||||||
<Upload className="w-8 h-8 mb-2" />
|
|
||||||
<p className="font-semibold">Click or drag and drop to upload</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
</AccordionTrigger>
|
||||||
</div>
|
<AccordionContent className="px-6 pb-6">
|
||||||
</div>
|
<div className="space-y-6">
|
||||||
</CardContent>
|
<div className="space-y-2">
|
||||||
</Card>
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
{hasImages && (
|
<Label htmlFor="format">Format</Label>
|
||||||
<Card>
|
</TooltipTrigger>
|
||||||
<CardHeader>
|
<TooltipContent>
|
||||||
<div className="flex justify-between items-center">
|
<p>Choose the output file format for the images.</p>
|
||||||
<CardTitle>Uploaded Images</CardTitle>
|
</TooltipContent>
|
||||||
<div className="flex items-center gap-2">
|
</Tooltip>
|
||||||
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button>
|
<Select value={format} onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)}>
|
||||||
<Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
|
<SelectTrigger id="format"><SelectValue placeholder="Select format" /></SelectTrigger>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<SelectContent>
|
||||||
{isConverting ? "Converting..." : `Download All (${images.length})`}
|
<SelectItem value="png">PNG</SelectItem>
|
||||||
</Button>
|
<SelectItem value="jpeg">JPEG</SelectItem>
|
||||||
</div>
|
<SelectItem value="webp">WEBP</SelectItem>
|
||||||
</div>
|
</SelectContent>
|
||||||
</CardHeader>
|
</Select>
|
||||||
<CardContent>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
{previewUrls.map((url, index) => {
|
<div className="flex justify-between items-center">
|
||||||
const baseFilename = generateFinalFilename(index);
|
<Tooltip>
|
||||||
const dimensionSuffix = width && height ? `_${width}x${height}` : '';
|
<TooltipTrigger asChild>
|
||||||
const finalFilename = `${baseFilename}${dimensionSuffix}`;
|
<Label htmlFor="quality">Quality</Label>
|
||||||
return (
|
</TooltipTrigger>
|
||||||
<div key={url} className="p-4 border rounded-lg flex items-center gap-4">
|
<TooltipContent>
|
||||||
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
<p>Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.</p>
|
||||||
<div className="flex-1 min-w-0">
|
</TooltipContent>
|
||||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
|
</Tooltip>
|
||||||
<Input
|
<span className="text-sm text-muted-foreground">{quality}%</span>
|
||||||
id={`filename-${index}`}
|
|
||||||
value={filenames[index]}
|
|
||||||
onChange={(e) => handleFilenameChange(index, e.target.value)}
|
|
||||||
className="text-sm font-medium h-8 mt-1"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
|
|
||||||
Final name: {finalFilename}.{format}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center shrink-0">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="text-gray-500 hover:text-primary"
|
|
||||||
onClick={() => handleConvertAndDownloadSingle(index)}
|
|
||||||
disabled={isConverting || convertingIndex !== null}
|
|
||||||
title="Download this image"
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="shrink-0 text-gray-500 hover:text-destructive"
|
|
||||||
onClick={() => handleRemoveImage(index)}
|
|
||||||
disabled={isConverting || convertingIndex !== null}
|
|
||||||
title="Remove this image"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<Slider
|
||||||
})}
|
id="quality"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={[quality]}
|
||||||
|
onValueChange={(value) => setQuality(value[0])}
|
||||||
|
disabled={format === 'png'}
|
||||||
|
/>
|
||||||
|
{format === 'png' && (
|
||||||
|
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={handleApplySettings}
|
||||||
|
disabled={!hasImages}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
Apply Settings
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Confirm and apply all the settings above. This does not download the images.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-2 flex flex-col gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Upload Images</h3>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
|
||||||
|
isDraggingOver ? "border-primary bg-accent" : "border-input"
|
||||||
|
)}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
||||||
|
<Upload className="w-8 h-8 mb-2" />
|
||||||
|
<p className="font-semibold">Click or drag and drop to upload</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p>
|
||||||
|
</div>
|
||||||
|
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
|
{hasImages && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<CardTitle>Uploaded Images</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Remove all uploaded images.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
{isConverting ? "Converting..." : `Download All (${images.length})`}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Convert and download all images with the current settings.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{previewUrls.map((url, index) => {
|
||||||
|
const baseFilename = generateFinalFilename(index);
|
||||||
|
const dimensionSuffix = width && height ? `_${width}x${height}` : '';
|
||||||
|
const finalFilename = `${baseFilename}${dimensionSuffix}`;
|
||||||
|
return (
|
||||||
|
<div key={url} className="p-4 border rounded-lg flex items-center gap-4">
|
||||||
|
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
|
||||||
|
<Input
|
||||||
|
id={`filename-${index}`}
|
||||||
|
value={filenames[index]}
|
||||||
|
onChange={(e) => handleFilenameChange(index, e.target.value)}
|
||||||
|
className="text-sm font-medium h-8 mt-1"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
|
||||||
|
Final name: {finalFilename}.{format}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center shrink-0">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-gray-500 hover:text-primary"
|
||||||
|
onClick={() => handleConvertAndDownloadSingle(index)}
|
||||||
|
disabled={isConverting || convertingIndex !== null}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Download this image</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="shrink-0 text-gray-500 hover:text-destructive"
|
||||||
|
onClick={() => handleRemoveImage(index)}
|
||||||
|
disabled={isConverting || convertingIndex !== null}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Remove this image</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user