feat: add frontend Svelte app with resize and crop UI

This commit is contained in:
2026-03-07 23:03:00 -06:00
parent 1b5b803b27
commit b378965b27

158
frontend/src/App.svelte Normal file
View File

@@ -0,0 +1,158 @@
<script lang="ts">
import { transformImage } from "./lib/api";
let file: File | null = null;
let width: number | null = null;
let height: number | null = null;
let quality = 80;
let format: "png" | "webp" | "jpeg" = "png";
// cropping / resizing
let fit: "inside" | "cover" = "inside"; // inside = resize only, cover = crop
let position:
| "center"
| "top"
| "right"
| "bottom"
| "left"
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right" = "center";
let processing = false;
let error: string | null = null;
async function onSubmit() {
if (!file) {
error = "Please select an image file";
return;
}
error = null;
processing = true;
try {
const blob = await transformImage(file, {
width: width || undefined,
height: height || undefined,
quality,
format,
fit,
position
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `output.${format}`;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
error = "Processing failed";
console.error(e);
} finally {
processing = false;
}
}
function onFileChange(e: Event) {
const target = e.target as HTMLInputElement;
file = target.files?.[0] || null;
}
</script>
<main>
<h1>PNG Editor</h1>
<input type="file" accept="image/*" on:change={onFileChange} />
<div>
<label>Width: <input type="number" bind:value={width} min="1" /></label>
<label>Height: <input type="number" bind:value={height} min="1" /></label>
</div>
<div>
<label>Fit mode:
<select bind:value={fit}>
<option value="inside">Resize only (no crop)</option>
<option value="cover">Crop to fit box</option>
</select>
</label>
</div>
{#if fit === "cover"}
<div>
<label>Crop position:
<select bind:value={position}>
<option value="center">Center</option>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
<option value="right">Right</option>
<option value="top-left">Top-left</option>
<option value="top-right">Top-right</option>
<option value="bottom-left">Bottom-left</option>
<option value="bottom-right">Bottom-right</option>
</select>
</label>
</div>
{/if}
<div>
<label>Quality:
<input type="range" min="10" max="100" bind:value={quality} />
</label>
<span>{quality}</span>
</div>
<div>
<label>Format:
<select bind:value={format}>
<option value="png">PNG</option>
<option value="webp">WebP</option>
<option value="jpeg">JPEG</option>
</select>
</label>
</div>
{#if error}
<p style="color: red">{error}</p>
{/if}
<button on:click|preventDefault={onSubmit} disabled={processing}>
{processing ? "Processing..." : "Transform & Download"}
</button>
</main>
<style>
main {
max-width: 600px;
margin: 2rem auto;
padding: 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
margin-bottom: 2rem;
}
label {
display: block;
margin: 1rem 0;
}
input[type="number"],
select {
margin-left: 0.5rem;
}
button {
margin-top: 1.5rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>