(function (global) { "use strict"; const STYLE_ID = "visitor-counter-styles"; function injectStyles() { if (document.getElementById(STYLE_ID)) return; const s = document.createElement("style"); s.id = STYLE_ID; s.textContent = ` .vc-root { display: inline-flex; flex-direction: column; align-items: center; gap: 6px; font-family: "Comic Code"; } .vc-digits { display: flex; align-items: center; gap: 4px; min-height: 50px; } .vc-digits img { display: block; image-rendering: pixelated; width: 22.5px; height: 50px; } .vc-label { font-size: 11px; color: var(--ch-muted, #8b95a1); letter-spacing: 0.04em; text-transform: lowercase; } .vc-error { font-size: 11px; color: var(--ch-muted, #8b95a1); } @media (prefers-reduced-motion: reduce) { .vc-digits img { animation: none !important; } } `; document.head.appendChild(s); } /** * Returns the cached visitor count for this session, or null if not cached. * The cache key is scoped to namespace+key so multiple counters don't collide. */ function getCached(namespace, key) { try { const raw = localStorage.getItem("vc:" + namespace + ":" + key); if (!raw) return null; const { count, session } = JSON.parse(raw); // Use sessionStorage token to detect new tabs vs. refreshes. // A refresh keeps the same sessionStorage; a new tab starts fresh. const token = sessionStorage.getItem("vc-session"); if (token && token === session) return count; return null; } catch (_) { return null; } } function setCached(namespace, key, count) { try { // Create (or reuse) a session token so this count is tied to the tab session. let token = sessionStorage.getItem("vc-session"); if (!token) { token = Math.random().toString(36).slice(2); sessionStorage.setItem("vc-session", token); } localStorage.setItem( "vc:" + namespace + ":" + key, JSON.stringify({ count, session: token }) ); } catch (_) { } } async function fetchCount(namespace, key) { const url = `https://abacus.jasoncameron.dev/hit/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`; const res = await fetch(url); if (!res.ok) throw new Error("Abacus HTTP " + res.status); const data = await res.json(); // Abacus returns { value: } if (typeof data.value !== "number") throw new Error("Unexpected response shape"); return data.value; } function renderDigits(container, count, imgPath, imgExt) { container.innerHTML = ""; const digits = String(Math.max(0, Math.floor(count))).padStart(6, "0"); for (const d of digits) { const img = document.createElement("img"); img.src = imgPath + d + imgExt; img.alt = d; img.width = 22.5; img.height = 50; container.appendChild(img); } } async function render(target, options) { const opts = Object.assign( { namespace: "clove-is-a-dev", key: "hits", imgPath: "/assets/numbers/", imgExt: ".png", label: "visitors", }, options || {} ); const root = typeof target === "string" ? document.querySelector(target) : target; if (!root) { console.warn("[visitor-counter] target not found:", target); return; } injectStyles(); root.classList.add("vc-root"); root.innerHTML = ""; const digitsEl = document.createElement("div"); digitsEl.className = "vc-digits"; root.appendChild(digitsEl); if (opts.label) { const labelEl = document.createElement("span"); labelEl.className = "vc-label"; labelEl.textContent = opts.label; root.appendChild(labelEl); } // Try cache first — avoids hitting the API (and incrementing) on refresh const cached = getCached(opts.namespace, opts.key); if (cached !== null) { renderDigits(digitsEl, cached, opts.imgPath, opts.imgExt); return; } // First visit in this session: hit the API try { const count = await fetchCount(opts.namespace, opts.key); setCached(opts.namespace, opts.key, count); renderDigits(digitsEl, count, opts.imgPath, opts.imgExt); } catch (err) { console.error("[visitor-counter]", err); const errEl = document.createElement("span"); errEl.className = "vc-error"; errEl.textContent = "— visitors"; digitsEl.replaceWith(errEl); if (opts.label && root.querySelector(".vc-label")) { root.querySelector(".vc-label").remove(); } } } // Auto-init from script tag attributes function autoInit() { const script = document.currentScript || document.querySelector('script[src*="visitor-counter"]'); if (!script) return; const targetSel = script.dataset.target; if (!targetSel) return; const ns = script.dataset.namespace; const key = script.dataset.key; const imgPath = script.dataset.imgPath; const imgExt = script.dataset.imgExt; const label = script.dataset.label; const init = () => render(targetSel, { ...(ns && { namespace: ns }), ...(key && { key }), ...(imgPath && { imgPath }), ...(imgExt && { imgExt }), ...(label !== undefined && { label }), }); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } } autoInit(); global.VisitorCounter = { render }; })(typeof window !== "undefined" ? window : this);