feat: add frontend Svelte app with resize and crop UI
This commit is contained in:
158
frontend/src/App.svelte
Normal file
158
frontend/src/App.svelte
Normal 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>
|
||||||
Reference in New Issue
Block a user