feat: add interactive resize/convert options (width, height, format, quality)

Resize page: width/height number inputs, aspect ratio lock, quality slider
Convert page: target format dropdown (JPEG/PNG/WebP/GIF/BMP), quality slider

Co-Authored-By: Kilo <kilo@kilo.ai>
This commit is contained in:
asepharyana
2026-07-24 19:07:05 +07:00
co-authored by Kilo
parent 5a64048ba2
commit 1a606d5c64
2 changed files with 140 additions and 56 deletions
+64 -28
View File
@@ -11,14 +11,25 @@ import { ResultPreview } from "@/components/tools/result-preview";
type PageState = "upload" | "processing" | "result" | "error";
const FORMATS = [
{ value: "jpeg", label: "JPEG (.jpg)" },
{ value: "png", label: "PNG (.png)" },
{ value: "webp", label: "WebP (.webp)" },
{ value: "gif", label: "GIF (.gif)" },
{ value: "bmp", label: "BMP (.bmp)" },
];
export default function ImageConvertPage() {
const [pageState, setPageState] = useState<PageState>("upload");
const [jobId, setJobId] = useState<string | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [format, setFormat] = useState("jpeg");
const [quality, setQuality] = useState(85);
const { upload } = useUpload({
tool: "image-convert",
options: { quality: 80 },
options: { format, quality },
});
const handleComplete = useCallback(() => setPageState("result"), []);
@@ -56,48 +67,73 @@ export default function ImageConvertPage() {
return (
<ToolLayout
title="Convert Image"
description="Convert HEIC->JPEG, PNG->WebP"
description="Konversi antar format gambar — atur format tujuan dan kualitas"
icon={Repeat}
phase={1}
>
{pageState === "upload" && (
<UploadZone
accept="image/*"
tool="image-convert"
onUpload={handleUpload}
/>
<div className="space-y-6">
{/* Format + Quality */}
<div className="p-6 rounded-xl border glass space-y-4">
<h3 className="font-semibold">Conversion Settings</h3>
<label className="space-y-1">
<span className="text-sm text-muted-foreground">Target Format</span>
<select
value={format}
onChange={(e) => setFormat(e.target.value)}
className="w-full bg-muted border rounded px-3 py-2 text-sm"
>
{FORMATS.map((f) => (
<option key={f.value} value={f.value}>
{f.label}
</option>
))}
</select>
</label>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Quality</span>
<span className="font-mono">{quality}%</span>
</div>
<input
type="range"
min={1}
max={100}
value={quality}
onChange={(e) => setQuality(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Small file</span>
<span>Best quality</span>
</div>
</div>
</div>
<UploadZone
accept="image/*"
tool="image-convert"
onUpload={handleUpload}
/>
</div>
)}
{pageState === "processing" && jobId && (
<ProgressBar
progress={progress}
stage={stage}
message={message}
status={status}
onRetry={handleRetry}
/>
<ProgressBar progress={progress} stage={stage} message={message} status={status} onRetry={handleRetry} />
)}
{pageState === "result" && result && (
<ResultPreview
result={result}
onProcessAnother={handleRetry}
/>
<ResultPreview result={result} onProcessAnother={handleRetry} />
)}
{pageState === "error" && (
<div className="p-6 rounded-xl border border-destructive/20 bg-destructive/5 text-center">
<p className="text-destructive font-medium mb-4">
{errorMsg || "Terjadi kesalahan"}
</p>
<button
onClick={handleRetry}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90"
>
Coba Lagi
</button>
<p className="text-destructive font-medium mb-4">{errorMsg || "Terjadi kesalahan"}</p>
<button onClick={handleRetry} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90">Coba Lagi</button>
</div>
)}
</ToolLayout>
);
}
}
+76 -28
View File
@@ -16,9 +16,14 @@ export default function ImageResizePage() {
const [jobId, setJobId] = useState<string | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [width, setWidth] = useState(1920);
const [height, setHeight] = useState(1080);
const [lockAspect, setLockAspect] = useState(true);
const [quality, setQuality] = useState(85);
const { upload } = useUpload({
tool: "image-resize",
options: { quality: 80 },
options: { width, height, quality, fit: lockAspect ? "inside" : "fill" },
});
const handleComplete = useCallback(() => setPageState("result"), []);
@@ -56,48 +61,91 @@ export default function ImageResizePage() {
return (
<ToolLayout
title="Resize Image"
description="Ubah dimensi gambar"
description="Ubah dimensi gambar — atur lebar, tinggi, dan kualitas"
icon={Crop}
phase={1}
>
{pageState === "upload" && (
<UploadZone
accept="image/*"
tool="image-resize"
onUpload={handleUpload}
/>
<div className="space-y-6">
{/* Dimensions */}
<div className="p-6 rounded-xl border glass space-y-4">
<h3 className="font-semibold">Dimensions</h3>
<div className="grid grid-cols-2 gap-4">
<label className="space-y-1">
<span className="text-sm text-muted-foreground">Width (px)</span>
<input
type="number"
value={width}
onChange={(e) => setWidth(Number(e.target.value))}
min={1}
max={10000}
className="w-full bg-muted border rounded px-3 py-2 text-sm"
/>
</label>
<label className="space-y-1">
<span className="text-sm text-muted-foreground">Height (px)</span>
<input
type="number"
value={height}
onChange={(e) => setHeight(Number(e.target.value))}
min={1}
max={10000}
className="w-full bg-muted border rounded px-3 py-2 text-sm"
/>
</label>
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={lockAspect}
onChange={(e) => setLockAspect(e.target.checked)}
/>
Lock aspect ratio
</label>
</div>
{/* Quality */}
<div className="p-6 rounded-xl border glass space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Quality</span>
<span className="text-sm font-mono text-muted-foreground">{quality}%</span>
</div>
<input
type="range"
min={1}
max={100}
value={quality}
onChange={(e) => setQuality(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Small file</span>
<span>Best quality</span>
</div>
</div>
<UploadZone
accept="image/*"
tool="image-resize"
onUpload={handleUpload}
/>
</div>
)}
{pageState === "processing" && jobId && (
<ProgressBar
progress={progress}
stage={stage}
message={message}
status={status}
onRetry={handleRetry}
/>
<ProgressBar progress={progress} stage={stage} message={message} status={status} onRetry={handleRetry} />
)}
{pageState === "result" && result && (
<ResultPreview
result={result}
onProcessAnother={handleRetry}
/>
<ResultPreview result={result} onProcessAnother={handleRetry} />
)}
{pageState === "error" && (
<div className="p-6 rounded-xl border border-destructive/20 bg-destructive/5 text-center">
<p className="text-destructive font-medium mb-4">
{errorMsg || "Terjadi kesalahan"}
</p>
<button
onClick={handleRetry}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90"
>
Coba Lagi
</button>
<p className="text-destructive font-medium mb-4">{errorMsg || "Terjadi kesalahan"}</p>
<button onClick={handleRetry} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90">Coba Lagi</button>
</div>
)}
</ToolLayout>
);
}
}