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:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user