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"}` }), ); }