| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { SITE, ENDPOINTS, TOOL_CATALOG, siteUrl } from "./config.js"; |
| import { RateLimitError } from "./api-client.js"; |
|
|
| |
| export function el(tag, props = {}, ...children) { |
| const node = document.createElement(tag); |
| for (const [key, value] of Object.entries(props || {})) { |
| if (value == null) continue; |
| if (key === "className") node.className = value; |
| else if (key === "text") node.textContent = value; |
| else if (key === "html") node.innerHTML = value; |
| else if (key === "dataset") Object.assign(node.dataset, value); |
| else if (key === "style" && typeof value === "object") Object.assign(node.style, value); |
| else if (key.startsWith("on") && typeof value === "function") { |
| node.addEventListener(key.slice(2).toLowerCase(), value); |
| } else node.setAttribute(key, value); |
| } |
| for (const child of children.flat()) { |
| if (child == null || child === false) continue; |
| node.appendChild(typeof child === "string" ? document.createTextNode(child) : child); |
| } |
| return node; |
| } |
|
|
| function header() { |
| return el("header", { className: "ms-header" }, |
| el("a", { className: "ms-brand", href: SITE.origin, target: "_blank", rel: "noopener" }, |
| el("span", { className: "ms-brand-mark", text: "M" }), |
| el("span", { className: "ms-brand-name", text: SITE.name }), |
| ), |
| el("a", { className: "ms-header-link", href: SITE.origin, target: "_blank", rel: "noopener", |
| html: "All tools <span aria-hidden=\"true\">↗</span>" }), |
| ); |
| } |
|
|
| function crossSell(toolKey) { |
| const items = TOOL_CATALOG.filter((t) => t.key !== toolKey); |
| return el("footer", { className: "ms-footer" }, |
| el("p", { className: "ms-footer-title", text: "More free tools on MesmerTools" }), |
| el("div", { className: "ms-grid" }, |
| ...items.map((t) => |
| el("a", { className: "ms-tool-card", href: siteUrl(t.path), target: "_blank", rel: "noopener" }, |
| el("span", { className: "ms-tool-emoji", text: t.emoji }), |
| el("span", { className: "ms-tool-text" }, |
| el("span", { className: "ms-tool-name", text: t.name }), |
| el("span", { className: "ms-tool-desc", text: t.desc }), |
| ), |
| ), |
| ), |
| ), |
| el("p", { className: "ms-powered" }, |
| "Powered by ", |
| el("a", { href: SITE.origin, target: "_blank", rel: "noopener", text: "mesmer.tools" }), |
| " — runs on the public MesmerTools API.", |
| ), |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function createSpace(opts) { |
| const { toolKey, emoji, title, subtitle, intro } = opts; |
| const endpoint = ENDPOINTS[toolKey] || {}; |
| const limit = endpoint.freeLimitPerHour; |
| const limitNote = limit |
| ? `${limit} free requests/hour per user · higher limits on mesmer.tools` |
| : `Free · backed by mesmer.tools`; |
|
|
| const root = document.getElementById("app") || document.body; |
|
|
| const status = el("div", { className: "ms-status", role: "status", "aria-live": "polite" }); |
| const form = el("form", { className: "ms-form" }); |
| const output = el("div", { className: "ms-output" }); |
|
|
| const hero = el("section", { className: "ms-hero" }, |
| el("div", { className: "ms-hero-emoji", text: emoji }), |
| el("h1", { className: "ms-title", text: title }), |
| el("p", { className: "ms-subtitle", text: subtitle }), |
| intro ? el("p", { className: "ms-intro", text: intro }) : null, |
| el("p", { className: "ms-limit-note", text: limitNote }), |
| ); |
|
|
| const card = el("section", { className: "ms-card" }, form, status, output); |
| const main = el("main", { className: "ms-main" }, hero, card); |
|
|
| root.appendChild(header()); |
| root.appendChild(main); |
| root.appendChild(crossSell(toolKey)); |
|
|
| function clearStatus() { |
| status.replaceChildren(); |
| } |
|
|
| function showRateLimit() { |
| const full = siteUrl(endpoint.fullToolPath || "/"); |
| const msg = limit |
| ? `You've used your ${limit} free requests this hour. Each visitor gets their own quota — ` |
| : `You've hit a temporary limit — `; |
| clearStatus(); |
| status.appendChild( |
| el("div", { className: "ms-banner ms-banner-limit" }, |
| el("strong", { text: "Free limit reached. " }), |
| msg, |
| el("a", { className: "ms-banner-cta", href: full, target: "_blank", rel: "noopener", |
| text: "use the full tool on mesmer.tools →" }), |
| ), |
| ); |
| } |
|
|
| |
| function showError(err) { |
| if (err instanceof RateLimitError || (err && err.isRateLimit)) { |
| showRateLimit(); |
| return; |
| } |
| const text = typeof err === "string" ? err : (err && err.message) || "Something went wrong."; |
| clearStatus(); |
| status.appendChild(el("div", { className: "ms-banner ms-banner-error", text })); |
| } |
|
|
| function setLoading(isLoading, label) { |
| card.classList.toggle("is-loading", !!isLoading); |
| if (isLoading) { |
| clearStatus(); |
| status.appendChild( |
| el("div", { className: "ms-banner ms-banner-loading" }, |
| el("span", { className: "ms-spinner", "aria-hidden": "true" }), |
| el("span", { text: label || "Working…" }), |
| ), |
| ); |
| } else { |
| clearStatus(); |
| } |
| } |
|
|
| function clearOutput() { |
| output.replaceChildren(); |
| } |
|
|
| return { form, output, status, setLoading, showError, showRateLimit, clearStatus, clearOutput, limitNote }; |
| } |
|
|
| |
| export function mountChrome(toolKey) { |
| const root = document.getElementById("app") || document.body; |
| root.prepend(header()); |
| root.appendChild(crossSell(toolKey)); |
| } |
|
|