diff --git a/src/components/filename-settings.tsx b/src/components/filename-settings.tsx new file mode 100644 index 0000000..488a845 --- /dev/null +++ b/src/components/filename-settings.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Label } from "@/components/ui/label"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { HelpCircle } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +interface FilenameSettingsProps { + useDefaultBaseName: boolean; + onUseDefaultBaseNameChange: (checked: boolean) => void; + defaultBaseName: string; + onDefaultBaseNameChange: (e: React.ChangeEvent) => void; + onApplyDefaultBaseNameToAll: () => void; + hasImages: boolean; + prefix: string; + onPrefixChange: (e: React.ChangeEvent) => void; + suffix: string; + onSuffixChange: (e: React.ChangeEvent) => void; + useCounter: boolean; + onUseCounterChange: (checked: boolean) => void; + counterStart: number; + onCounterStartChange: (e: React.ChangeEvent) => void; + counterDigits: number; + onCounterDigitsChange: (e: React.ChangeEvent) => void; +} + +export function FilenameSettings({ + useDefaultBaseName, + onUseDefaultBaseNameChange, + defaultBaseName, + onDefaultBaseNameChange, + onApplyDefaultBaseNameToAll, + hasImages, + prefix, + onPrefixChange, + suffix, + onSuffixChange, + useCounter, + onUseCounterChange, + counterStart, + onCounterStartChange, + counterDigits, + onCounterDigitsChange, +}: FilenameSettingsProps) { + const t = useTranslations("ImageConverter"); + + return ( + + +
+

+ {t("filenameSettingsTitle")} +

+

+ {t("filenameSettingsSubtitle")} +

+
+
+ +
+
+ + +
+ {useDefaultBaseName && ( +
+ +
+ + + + + + +

{t("applyToAllTooltip")}

+
+
+
+
+ )} +
+
+ + + + + + +

{t("prefixTooltip")}

+
+
+
+ +
+
+
+ + + + + + +

{t("suffixTooltip")}

+
+
+
+ +
+
+ + +
+ {useCounter && ( +
+
+
+ + + + + + +

{t("counterStartTooltip")}

+
+
+
+ +
+
+
+ + + + + + +

{t("counterDigitsTooltip")}

+
+
+
+ +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/image-converter.tsx b/src/components/image-converter.tsx index 85f0c3a..008a5c5 100644 --- a/src/components/image-converter.tsx +++ b/src/components/image-converter.tsx @@ -1,42 +1,12 @@ "use client"; -import { useState, useRef, ChangeEvent, useEffect } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Upload, Download, X, Trash2, Check, ArrowRightLeft, HelpCircle, RotateCcw } from "lucide-react"; +import { useState, useEffect, ChangeEvent } from "react"; import { toast } from "sonner"; -import { cn } from "@/lib/utils"; -import { Switch } from "@/components/ui/switch"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { Slider } from "@/components/ui/slider"; -import { ObjectPositionControl } from "./object-position-control"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { TooltipProvider } from "@/components/ui/tooltip"; import { useTranslations } from "next-intl"; +import { ImageUpload } from "./image-upload"; +import { UploadedImageList } from "./uploaded-image-list"; +import { SettingsPanel } from "./settings-panel"; const initialSettings = { width: "", @@ -59,14 +29,6 @@ const initialSettings = { export function ImageConverter() { const t = useTranslations("ImageConverter"); - const aspectRatios = [ - { name: t("aspectRatios.custom"), value: "custom" }, - { name: t("aspectRatios.square"), value: "1/1" }, - { name: t("aspectRatios.standard"), value: "4/3" }, - { name: t("aspectRatios.photography"), value: "3/2" }, - { name: t("aspectRatios.widescreen"), value: "16/9" }, - ]; - const [images, setImages] = useState([]); const [previewUrls, setPreviewUrls] = useState([]); const [filenames, setFilenames] = useState([]); @@ -76,7 +38,6 @@ export function ImageConverter() { const [keepOrientation, setKeepOrientation] = useState(initialSettings.keepOrientation); const [format, setFormat] = useState<"png" | "jpeg" | "webp">(initialSettings.format); const [quality, setQuality] = useState(initialSettings.quality); - const [prefix, setPrefix] = useState(initialSettings.prefix); const [suffix, setSuffix] = useState(initialSettings.suffix); const [useCounter, setUseCounter] = useState(initialSettings.useCounter); @@ -84,14 +45,11 @@ export function ImageConverter() { const [counterDigits, setCounterDigits] = useState(initialSettings.counterDigits); const [useDefaultBaseName, setUseDefaultBaseName] = useState(initialSettings.useDefaultBaseName); const [defaultBaseName, setDefaultBaseName] = useState(initialSettings.defaultBaseName); - const [scaleMode, setScaleMode] = useState<'fill' | 'cover' | 'contain'>(initialSettings.scaleMode); const [objectPosition, setObjectPosition] = useState(initialSettings.objectPosition); - const [isConverting, setIsConverting] = useState(false); const [convertingIndex, setConvertingIndex] = useState(null); const [isDraggingOver, setIsDraggingOver] = useState(false); - const fileInputRef = useRef(null); useEffect(() => { return () => { @@ -132,11 +90,6 @@ export function ImageConverter() { toast.success(t("toasts.imagesAdded", { count: imageFiles.length })); }; - const handleImageChange = (e: ChangeEvent) => { - handleFiles(e.target.files); - if (e.target) e.target.value = ""; - }; - const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(true); @@ -155,13 +108,9 @@ export function ImageConverter() { const handleRemoveImage = (indexToRemove: number) => { URL.revokeObjectURL(previewUrls[indexToRemove]); - const newImages = images.filter((_, i) => i !== indexToRemove); - const newPreviewUrls = previewUrls.filter((_, i) => i !== indexToRemove); - const newFilenames = filenames.filter((_, i) => i !== indexToRemove); - - setImages(newImages); - setPreviewUrls(newPreviewUrls); - setFilenames(newFilenames); + setImages((prev) => prev.filter((_, i) => i !== indexToRemove)); + setPreviewUrls((prev) => prev.filter((_, i) => i !== indexToRemove)); + setFilenames((prev) => prev.filter((_, i) => i !== indexToRemove)); }; const handleClearAll = () => { @@ -175,9 +124,11 @@ export function ImageConverter() { }; const handleFilenameChange = (index: number, newName: string) => { - const newFilenames = [...filenames]; - newFilenames[index] = newName; - setFilenames(newFilenames); + setFilenames((prev) => { + const newFilenames = [...prev]; + newFilenames[index] = newName; + return newFilenames; + }); }; const generateFinalFilename = (index: number) => { @@ -307,19 +258,13 @@ export function ImageConverter() { setIsConverting(true); toast.info(t("toasts.conversionStarting", { count: images.length })); - const conversionPromises = images.map((image, index) => - convertAndDownload(image, previewUrls[index], index) - ); - try { - await Promise.all(conversionPromises); + await Promise.all(images.map((image, index) => + convertAndDownload(image, previewUrls[index], index) + )); toast.success(t("toasts.conversionSuccess", { count: images.length })); } catch (error) { - if (error instanceof Error) { - toast.error(error.message); - } else { - toast.error(t("toasts.conversionError")); - } + toast.error(error instanceof Error ? error.message : t("toasts.conversionError")); } finally { setIsConverting(false); } @@ -333,11 +278,7 @@ export function ImageConverter() { await convertAndDownload(images[index], previewUrls[index], index); toast.success(t("toasts.singleConversionSuccess", { filename: filenames[index] })); } catch (error) { - if (error instanceof Error) { - toast.error(error.message); - } else { - toast.error(t("toasts.conversionError")); - } + toast.error(error instanceof Error ? error.message : t("toasts.conversionError")); } finally { setConvertingIndex(null); } @@ -368,36 +309,21 @@ export function ImageConverter() { const handleAspectRatioChange = (value: string) => { setAspectRatio(value); - - if (value === "custom") { - return; - } + if (value === "custom") return; const [w, h] = value.split("/").map(Number); - let newWidth: number; - let newHeight: number; - - if (w > h) { - newWidth = 1000; - newHeight = Math.round((1000 * h) / w); - } else if (h > w) { - newHeight = 1000; - newWidth = Math.round((1000 * w) / h); - } else { - newWidth = 1000; - newHeight = 1000; - } - - setWidth(newWidth); - setHeight(newHeight); + const newWidth = w > h ? 1000 : Math.round((1000 * w) / h); + const newHeight = h > w ? 1000 : Math.round((1000 * h) / w); + setWidth(w === h ? 1000 : newWidth); + setHeight(w === h ? 1000 : newHeight); }; - const handleWidthChange = (e: React.ChangeEvent) => { + const handleWidthChange = (e: ChangeEvent) => { setWidth(e.target.value); setAspectRatio("custom"); }; - const handleHeightChange = (e: React.ChangeEvent) => { + const handleHeightChange = (e: ChangeEvent) => { setHeight(e.target.value); setAspectRatio("custom"); }; @@ -407,19 +333,16 @@ export function ImageConverter() { setHeight(width); }; - const hasImages = images.length > 0; - const handleApplyDefaultBaseNameToAll = () => { if (!defaultBaseName) { toast.error(t("toasts.defaultBaseNameMissing")); return; } - if (!hasImages) { + if (images.length === 0) { toast.info(t("toasts.uploadFirst")); return; } - const newFilenames = filenames.map(() => defaultBaseName); - setFilenames(newFilenames); + setFilenames(filenames.map(() => defaultBaseName)); toast.success(t("toasts.baseNameApplied", { name: defaultBaseName, count: images.length })); }; @@ -427,484 +350,65 @@ export function ImageConverter() {
- - -
-

{t("uploadTitle")}

-
fileInputRef.current?.click()} - > -
- -

{t("uploadPrompt")}

-

{t("uploadHint")}

-
- -
-
-
-
- - {hasImages && ( - - -
- {t("uploadedImagesTitle")} -
- - - - - -

{t("clearAllTooltip")}

-
-
- - - - - -

{t("downloadAllTooltip")}

-
-
-
-
-
- -
- {previewUrls.map((url, index) => { - const finalFilename = generateFinalFilename(index); - return ( -
- {`Preview -
- - handleFilenameChange(index, e.target.value)} - className="text-sm font-medium h-8 mt-1" - /> -

- {t("finalNameLabel", { filename: `${finalFilename}.${format}` })} -

-
-
- - - - - -

{t("downloadSingleTooltip")}

-
-
- - - - - -

{t("removeSingleTooltip")}

-
-
-
-
- ); - })} -
-
-
- )} + +
-
- - - -
-

{t("imageSettingsTitle")}

-

- {t("imageSettingsSubtitle")} -

-
-
- -
-
-
- - - - - - -

{t("aspectRatioTooltip")}

-
-
-
- -
-
-
-
- - - - - - -

{t("widthTooltip")}

-
-
-
- -
- - - - - -

{t("swapDimensionsTooltip")}

-
-
-
-
- - - - - - -

{t("heightTooltip")}

-
-
-
- -
-
-
- setKeepOrientation(Boolean(checked))} /> - -
-
-
-
- - - - - - -

{t("scalingTooltip")}

-
-
-
- -
- {scaleMode !== 'fill' && ( -
-
- - - - - - -

{t("positionTooltip")}

-
-
-
- setObjectPosition(pos)} /> -
- )} -
-
- - - -
-

{t("filenameSettingsTitle")}

-

{t("filenameSettingsSubtitle")}

-
-
- -
-
- - -
- {useDefaultBaseName && ( -
- -
- setDefaultBaseName(e.target.value)} - /> - - - - - -

{t("applyToAllTooltip")}

-
-
-
-
- )} -
-
- - - - - - -

{t("prefixTooltip")}

-
-
-
- setPrefix(e.target.value)} /> -
-
-
- - - - - - -

{t("suffixTooltip")}

-
-
-
- setSuffix(e.target.value)} /> -
-
- - -
- {useCounter && ( -
-
-
- - - - - - -

{t("counterStartTooltip")}

-
-
-
- setCounterStart(Math.max(0, Number(e.target.value)))} - min="0" - /> -
-
-
- - - - - - -

{t("counterDigitsTooltip")}

-
-
-
- setCounterDigits(Math.max(1, Number(e.target.value)))} - min="1" - /> -
-
- )} -
-
-
- - - -
-

{t("qualitySettingsTitle")}

-

{t("qualitySettingsSubtitle")}

-
-
- -
-
-
- - - - - - -

{t("formatTooltip")}

-
-
-
- -
-
-
-
- - - - - - -

{t("qualityTooltip")}

-
-
-
- {quality}% -
- setQuality(value[0])} - disabled={format === 'png'} - /> - {format === 'png' && ( -

{t("qualityDisabledHint")}

- )} -
-
-
-
-
-
- - - - - -

{t("resetButtonTooltip")}

-
-
- - - - - -

{t("applyButtonTooltip")}

-
-
-
-
+ setDefaultBaseName(e.target.value)} + onApplyDefaultBaseNameToAll={handleApplyDefaultBaseNameToAll} + hasImages={images.length > 0} + prefix={prefix} + onPrefixChange={(e) => setPrefix(e.target.value)} + suffix={suffix} + onSuffixChange={(e) => setSuffix(e.target.value)} + useCounter={useCounter} + onUseCounterChange={setUseCounter} + counterStart={counterStart} + onCounterStartChange={(e) => setCounterStart(Math.max(0, Number(e.target.value)))} + counterDigits={counterDigits} + onCounterDigitsChange={(e) => setCounterDigits(Math.max(1, Number(e.target.value)))} + format={format} + onFormatChange={setFormat} + quality={quality} + onQualityChange={setQuality} + onResetSettings={handleResetSettings} + onApplySettings={handleApplySettings} + />
); diff --git a/src/components/image-settings.tsx b/src/components/image-settings.tsx new file mode 100644 index 0000000..7eaa131 --- /dev/null +++ b/src/components/image-settings.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Label } from "@/components/ui/label"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { HelpCircle, ArrowRightLeft } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ObjectPositionControl } from "./object-position-control"; + +interface ImageSettingsProps { + aspectRatio: string; + onAspectRatioChange: (value: string) => void; + width: number | string; + onWidthChange: (e: React.ChangeEvent) => void; + onSwapDimensions: () => void; + height: number | string; + onHeightChange: (e: React.ChangeEvent) => void; + keepOrientation: boolean; + onKeepOrientationChange: (checked: boolean) => void; + scaleMode: 'fill' | 'cover' | 'contain'; + onScaleModeChange: (value: 'fill' | 'cover' | 'contain') => void; + objectPosition: string; + onObjectPositionChange: (pos: any) => void; +} + +export function ImageSettings({ + aspectRatio, + onAspectRatioChange, + width, + onWidthChange, + onSwapDimensions, + height, + onHeightChange, + keepOrientation, + onKeepOrientationChange, + scaleMode, + onScaleModeChange, + objectPosition, + onObjectPositionChange, +}: ImageSettingsProps) { + const t = useTranslations("ImageConverter"); + + const aspectRatios = [ + { name: t("aspectRatios.custom"), value: "custom" }, + { name: t("aspectRatios.square"), value: "1/1" }, + { name: t("aspectRatios.standard"), value: "4/3" }, + { name: t("aspectRatios.photography"), value: "3/2" }, + { name: t("aspectRatios.widescreen"), value: "16/9" }, + ]; + + return ( + + +
+

+ {t("imageSettingsTitle")} +

+

+ {t("imageSettingsSubtitle")} +

+
+
+ +
+
+
+ + + + + + +

{t("aspectRatioTooltip")}

+
+
+
+ +
+
+
+
+ + + + + + +

{t("widthTooltip")}

+
+
+
+ +
+ + + + + +

{t("swapDimensionsTooltip")}

+
+
+
+
+ + + + + + +

{t("heightTooltip")}

+
+
+
+ +
+
+
+ onKeepOrientationChange(Boolean(checked))} + /> + +
+
+
+
+ + + + + + +

{t("scalingTooltip")}

+
+
+
+ +
+ {scaleMode !== "fill" && ( +
+
+ + + + + + +

{t("positionTooltip")}

+
+
+
+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/image-upload.tsx b/src/components/image-upload.tsx new file mode 100644 index 0000000..f6ca428 --- /dev/null +++ b/src/components/image-upload.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useRef, ChangeEvent } from "react"; +import { useTranslations } from "next-intl"; +import { Upload } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent } from "@/components/ui/card"; + +interface ImageUploadProps { + onFiles: (files: FileList | null) => void; + isDraggingOver: boolean; + onDragOver: (e: React.DragEvent) => void; + onDragLeave: (e: React.DragEvent) => void; + onDrop: (e: React.DragEvent) => void; +} + +export function ImageUpload({ + onFiles, + isDraggingOver, + onDragOver, + onDragLeave, + onDrop, +}: ImageUploadProps) { + const t = useTranslations("ImageConverter"); + const fileInputRef = useRef(null); + + const handleImageChange = (e: ChangeEvent) => { + onFiles(e.target.files); + if (e.target) e.target.value = ""; + }; + + return ( + + +
+

{t("uploadTitle")}

+
fileInputRef.current?.click()} + > +
+ +

{t("uploadPrompt")}

+

{t("uploadHint")}

+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/quality-settings.tsx b/src/components/quality-settings.tsx new file mode 100644 index 0000000..29c9acc --- /dev/null +++ b/src/components/quality-settings.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Label } from "@/components/ui/label"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { HelpCircle } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Slider } from "@/components/ui/slider"; + +type Format = "png" | "jpeg" | "webp"; + +interface QualitySettingsProps { + format: Format; + onFormatChange: (value: Format) => void; + quality: number; + onQualityChange: (value: number) => void; +} + +export function QualitySettings({ + format, + onFormatChange, + quality, + onQualityChange, +}: QualitySettingsProps) { + const t = useTranslations("ImageConverter"); + + return ( + + +
+

+ {t("qualitySettingsTitle")} +

+

+ {t("qualitySettingsSubtitle")} +

+
+
+ +
+
+
+ + + + + + +

{t("formatTooltip")}

+
+
+
+ +
+
+
+
+ + + + + + +

{t("qualityTooltip")}

+
+
+
+ {quality}% +
+ onQualityChange(value[0])} + disabled={format === "png"} + /> + {format === "png" && ( +

+ {t("qualityDisabledHint")} +

+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/settings-panel.tsx b/src/components/settings-panel.tsx new file mode 100644 index 0000000..9383145 --- /dev/null +++ b/src/components/settings-panel.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { Accordion } from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { RotateCcw, Check } from "lucide-react"; +import { ImageSettings } from "./image-settings"; +import { FilenameSettings } from "./filename-settings"; +import { QualitySettings } from "./quality-settings"; + +interface SettingsPanelProps { + aspectRatio: string; + onAspectRatioChange: (value: string) => void; + width: number | string; + onWidthChange: (e: React.ChangeEvent) => void; + onSwapDimensions: () => void; + height: number | string; + onHeightChange: (e: React.ChangeEvent) => void; + keepOrientation: boolean; + onKeepOrientationChange: (checked: boolean) => void; + scaleMode: 'fill' | 'cover' | 'contain'; + onScaleModeChange: (value: 'fill' | 'cover' | 'contain') => void; + objectPosition: string; + onObjectPositionChange: (pos: any) => void; + useDefaultBaseName: boolean; + onUseDefaultBaseNameChange: (checked: boolean) => void; + defaultBaseName: string; + onDefaultBaseNameChange: (e: React.ChangeEvent) => void; + onApplyDefaultBaseNameToAll: () => void; + hasImages: boolean; + prefix: string; + onPrefixChange: (e: React.ChangeEvent) => void; + suffix: string; + onSuffixChange: (e: React.ChangeEvent) => void; + useCounter: boolean; + onUseCounterChange: (checked: boolean) => void; + counterStart: number; + onCounterStartChange: (e: React.ChangeEvent) => void; + counterDigits: number; + onCounterDigitsChange: (e: React.ChangeEvent) => void; + format: "png" | "jpeg" | "webp"; + onFormatChange: (value: "png" | "jpeg" | "webp") => void; + quality: number; + onQualityChange: (value: number) => void; + onResetSettings: () => void; + onApplySettings: () => void; +} + +export function SettingsPanel(props: SettingsPanelProps) { + const t = useTranslations("ImageConverter"); + + return ( +
+ + + + + +
+ + + + + +

{t("resetButtonTooltip")}

+
+
+ + + + + +

{t("applyButtonTooltip")}

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/uploaded-image-list.tsx b/src/components/uploaded-image-list.tsx new file mode 100644 index 0000000..9829c9b --- /dev/null +++ b/src/components/uploaded-image-list.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Download, Trash2, X } from "lucide-react"; + +interface UploadedImageListProps { + previewUrls: string[]; + filenames: string[]; + isConverting: boolean; + convertingIndex: number | null; + onClearAll: () => void; + onConvertAndDownloadAll: () => void; + onFilenameChange: (index: number, newName: string) => void; + generateFinalFilename: (index: number) => string; + format: string; + onConvertAndDownloadSingle: (index: number) => void; + onRemoveImage: (index: number) => void; +} + +export function UploadedImageList({ + previewUrls, + filenames, + isConverting, + convertingIndex, + onClearAll, + onConvertAndDownloadAll, + onFilenameChange, + generateFinalFilename, + format, + onConvertAndDownloadSingle, + onRemoveImage, +}: UploadedImageListProps) { + const t = useTranslations("ImageConverter"); + const hasImages = previewUrls.length > 0; + + if (!hasImages) { + return null; + } + + return ( + + + +
+ {t("uploadedImagesTitle")} +
+ + + + + +

{t("clearAllTooltip")}

+
+
+ + + + + +

{t("downloadAllTooltip")}

+
+
+
+
+
+ +
+ {previewUrls.map((url, index) => { + const finalFilename = generateFinalFilename(index); + return ( +
+ {`Preview +
+ + onFilenameChange(index, e.target.value)} + className="text-sm font-medium h-8 mt-1" + /> +

+ {t("finalNameLabel", { + filename: `${finalFilename}.${format}`, + })} +

+
+
+ + + + + +

{t("downloadSingleTooltip")}

+
+
+ + + + + +

{t("removeSingleTooltip")}

+
+
+
+
+ ); + })} +
+
+
+
+ ); +} \ No newline at end of file