Spaces:
Running
Running
| import { createSpace, el } from "./shared/ui.js"; | |
| import { callRest } from "./shared/api-client.js"; | |
| import { ENDPOINTS, siteUrl } from "./shared/config.js"; | |
| const TOOL_KEY = "screenshot"; | |
| const API_PATH = ENDPOINTS[TOOL_KEY].apiPath; // "/api/v1/screenshot" | |
| const FULL_TOOL_URL = siteUrl(ENDPOINTS[TOOL_KEY].fullToolPath); // mesmer.tools/api-tools/screenshot | |
| // Allowed dimension ranges (validated client-side before the API call). | |
| const WIDTH = { min: 320, max: 1920, default: 1280 }; | |
| const HEIGHT = { min: 240, max: 1080, default: 720 }; | |
| const space = createSpace({ | |
| toolKey: TOOL_KEY, | |
| emoji: "📸", | |
| title: "Free Website Screenshot API", | |
| subtitle: "Capture a screenshot of any website URL as an image. Paste a link, pick a size, get a JPG — free, no signup.", | |
| intro: "Turn any URL into an image right from your browser. Great for previews, thumbnails, and link cards.", | |
| }); | |
| /* --- Form ----------------------------------------------------------------- */ | |
| const urlInput = el("input", { | |
| className: "ms-input", | |
| type: "url", | |
| id: "url", | |
| name: "url", | |
| required: "", | |
| placeholder: "https://example.com", | |
| autocomplete: "off", | |
| autocapitalize: "off", | |
| spellcheck: "false", | |
| }); | |
| const widthInput = el("input", { | |
| className: "ms-input", | |
| type: "number", | |
| id: "width", | |
| name: "width", | |
| inputmode: "numeric", | |
| min: String(WIDTH.min), | |
| max: String(WIDTH.max), | |
| step: "1", | |
| placeholder: String(WIDTH.default), | |
| }); | |
| const heightInput = el("input", { | |
| className: "ms-input", | |
| type: "number", | |
| id: "height", | |
| name: "height", | |
| inputmode: "numeric", | |
| min: String(HEIGHT.min), | |
| max: String(HEIGHT.max), | |
| step: "1", | |
| placeholder: String(HEIGHT.default), | |
| }); | |
| const widthHint = el("span", { className: "ms-hint", text: `Optional · ${WIDTH.min}–${WIDTH.max}, default ${WIDTH.default}` }); | |
| const heightHint = el("span", { className: "ms-hint", text: `Optional · ${HEIGHT.min}–${HEIGHT.max}, default ${HEIGHT.default}` }); | |
| function preset(label, w, h) { | |
| return el("button", { | |
| className: "ms-btn-ghost", | |
| type: "button", | |
| onClick: () => { | |
| widthInput.value = String(w); | |
| heightInput.value = String(h); | |
| validate(widthInput, widthHint, WIDTH); | |
| validate(heightInput, heightHint, HEIGHT); | |
| }, | |
| }, label); | |
| } | |
| const submitBtn = el("button", { className: "ms-btn", type: "submit" }, "Capture screenshot"); | |
| space.form.append( | |
| el("div", { className: "ms-field" }, | |
| el("label", { for: "url", text: "Website URL" }), | |
| urlInput, | |
| el("span", { className: "ms-hint", text: "Any public page, e.g. https://github.com" }), | |
| ), | |
| el("div", { className: "ms-row" }, | |
| el("div", { className: "ms-field" }, | |
| el("label", { for: "width", text: "Width (px)" }), | |
| widthInput, | |
| widthHint, | |
| ), | |
| el("div", { className: "ms-field" }, | |
| el("label", { for: "height", text: "Height (px)" }), | |
| heightInput, | |
| heightHint, | |
| ), | |
| ), | |
| el("div", { className: "ms-field" }, | |
| el("label", { text: "Size presets" }), | |
| el("div", { className: "ms-presets" }, | |
| preset("Desktop 1280×720", 1280, 720), | |
| preset("Wide 1920×1080", 1920, 1080), | |
| preset("Mobile 390×844", 390, 844), | |
| ), | |
| ), | |
| submitBtn, | |
| el("p", { className: "ms-api-note" }, | |
| "Free demo: ", | |
| String(ENDPOINTS[TOOL_KEY].freeLimitPerHour), | |
| " screenshots/hour per visitor. Need more? ", | |
| el("a", { href: FULL_TOOL_URL, target: "_blank", rel: "noopener", text: "Higher limits on mesmer.tools →" }), | |
| ), | |
| ); | |
| // Live-validate dimension fields as the user edits them. | |
| widthInput.addEventListener("input", () => validate(widthInput, widthHint, WIDTH)); | |
| heightInput.addEventListener("input", () => validate(heightInput, heightHint, HEIGHT)); | |
| /* --- Validation ----------------------------------------------------------- */ | |
| /** | |
| * Validates a dimension input. Empty means "use the default". | |
| * Returns { value, ok }; on failure marks the hint red and value is null. | |
| */ | |
| function validate(input, hint, range) { | |
| const raw = input.value.trim(); | |
| if (raw === "") { | |
| resetHint(hint, range, "ok"); | |
| return { value: range.default, ok: true }; | |
| } | |
| const n = Number(raw); | |
| if (!Number.isInteger(n)) { | |
| setHint(hint, "Enter a whole number of pixels.", true); | |
| return { value: null, ok: false }; | |
| } | |
| if (n < range.min || n > range.max) { | |
| setHint(hint, `Must be between ${range.min} and ${range.max}px.`, true); | |
| return { value: null, ok: false }; | |
| } | |
| resetHint(hint, range, "ok"); | |
| return { value: n, ok: true }; | |
| } | |
| function resetHint(hint, range, _state) { | |
| const min = range === WIDTH ? WIDTH.min : HEIGHT.min; | |
| const max = range === WIDTH ? WIDTH.max : HEIGHT.max; | |
| setHint(hint, `Optional · ${min}–${max}, default ${range.default}`, false); | |
| } | |
| function setHint(hint, text, isError) { | |
| hint.textContent = text; | |
| hint.style.color = isError ? "#fca5a5" : ""; | |
| } | |
| /* --- Submit --------------------------------------------------------------- */ | |
| space.form.addEventListener("submit", async (e) => { | |
| e.preventDefault(); | |
| const url = urlInput.value.trim(); | |
| if (!url) { | |
| urlInput.focus(); | |
| return; | |
| } | |
| const w = validate(widthInput, widthHint, WIDTH); | |
| const h = validate(heightInput, heightHint, HEIGHT); | |
| if (!w.ok || !h.ok) { | |
| (!w.ok ? widthInput : heightInput).focus(); | |
| return; | |
| } | |
| space.clearOutput(); | |
| space.setLoading(true, "Capturing screenshot…"); | |
| submitBtn.disabled = true; | |
| try { | |
| const data = await callRest(API_PATH, { | |
| params: { url, width: w.value, height: h.value }, | |
| }); | |
| space.clearStatus(); | |
| renderResult(data); | |
| } catch (err) { | |
| space.clearOutput(); | |
| space.showError(err); | |
| } finally { | |
| submitBtn.disabled = false; | |
| } | |
| }); | |
| /* --- Result --------------------------------------------------------------- */ | |
| function renderResult(data) { | |
| // Success shape: { url, width, height, cached } | |
| const imgUrl = data.url; | |
| const width = data.width; | |
| const height = data.height; | |
| const filename = `screenshot-${width}x${height}.jpg`; | |
| space.output.append( | |
| el("div", { className: "ms-result-frame" }, | |
| el("img", { | |
| src: imgUrl, | |
| alt: `Screenshot of ${urlInput.value.trim()} at ${width}×${height}`, | |
| loading: "lazy", | |
| }), | |
| ), | |
| el("div", { className: "ms-result-actions" }, | |
| el("a", { className: "ms-btn-ghost", href: imgUrl, target: "_blank", rel: "noopener", text: "Open full size ↗" }), | |
| el("a", { className: "ms-btn-ghost", href: imgUrl, download: filename, text: "Download" }), | |
| ), | |
| el("p", { className: "ms-meta", | |
| text: `${width}×${height} · ${data.cached ? "served from cache" : "freshly captured"}` }), | |
| ); | |
| } | |