From c4bb8bdd752c52c3d1ac9d4b295ec01616ce2de8 Mon Sep 17 00:00:00 2001 From: David Ali Date: Wed, 7 Jan 2026 13:55:19 +0100 Subject: [PATCH] delete redundant file --- public/assets/wc-timer.js | 454 -------------------------------------- 1 file changed, 454 deletions(-) delete mode 100644 public/assets/wc-timer.js diff --git a/public/assets/wc-timer.js b/public/assets/wc-timer.js deleted file mode 100644 index 98dbe02..0000000 --- a/public/assets/wc-timer.js +++ /dev/null @@ -1,454 +0,0 @@ -/** - * wc-timer.js — Web Component - * --------------------------------------------- - * A tiny countdown/uptimer that renders as plain text - * (light DOM). Text is selectable and styles with normal CSS. - * - * Features: - * - Presets: auto | hms | dhms | ydhms | date - * - Auto locale (with override via locale/lang attrib) - * - Uses Intl.DurationFormat when available (fallback included) - * - "Compact" mode hides empty high units (e.g., 0 years) - * - One global, synced tick for all instances (no drift, low CPU) - * - Public JS API (properties): target, preset, labels, compact, locale - * - * Usage (HTML): - * - * - * - * - * Styling: - * wc-timer { font-size: 20px; color: #4da3ff; } - * - * JS control: - * const t = document.querySelector('wc-timer'); - * t.target = '2025-10-12T00:30:00Z'; // string | Date | ms number - * t.preset = 'dhms'; // 'auto' | 'hms' | 'dhms' | 'ydhms' | 'date' - * t.labels = 'short'; // 'none' | 'short' | 'long' - * t.compact = true; // boolean - * t.locale = 'pl'; // locale override - * - * Notes: - * - Don't mutate private fields (those starting with "_"). - * Use public setters (e.g., el.target = ...). - * - "auto" shows HH:MM:SS for near targets and adds days for far targets. - * - If DurationFormat is missing, we use a small fallback with plural rules. - */ - -/* ---------------- Global synced tick ---------------- - * One scheduler for all instances. It fires once per second, - * aligned to the next real second boundary. - * Saves CPU and avoids per-element setInterval drift. - */ -const TickHub = (() => { - const subs = new Set(); - let timer = null; - - function scheduleNext() { - // Align next tick to the next full second. - const now = performance.now(); - const msToNext = 1000 - (Math.floor(now) % 1000); - timer = setTimeout(() => { - const t = Date.now(); - // Notify all subscribers. Errors are isolated. - subs.forEach(cb => { try { cb(t); } catch { } }); - scheduleNext(); - }, msToNext); - } - - return { - /** - * sub(cb): subscribe to ticks. - * Returns an unsubscribe function. - */ - sub(cb) { - if (subs.size === 0) scheduleNext(); - subs.add(cb); - return () => { - subs.delete(cb); - if (!subs.size && timer) { clearTimeout(timer); timer = null; } - }; - } - }; -})(); - -/* ---------------- Locale helpers ---------------- - * We prefer: explicit element attribute -> nearest [lang] -> - * -> navigator.languages[0] -> navigator.language -> 'en' - */ -const hasIDF = typeof Intl !== 'undefined' && 'DurationFormat' in Intl; - -function resolveLocale(el) { - const attr = el.getAttribute('locale') || el.getAttribute('lang'); - const tree = el.closest?.('[lang]')?.getAttribute('lang') || document.documentElement.lang; - const nav = Array.isArray(navigator.languages) && navigator.languages.length - ? navigator.languages[0] - : navigator.language; - return (attr || tree || nav || 'en').toLowerCase(); -} - -// Fallback label dictionaries (English + Polish). -// Used only when Intl.DurationFormat is not available. -const LABELS = { - en: { - y: { long: { one: 'year', other: 'years' }, short: 'y' }, - M: { long: { one: 'month', other: 'months' }, short: 'mo' }, - d: { long: { one: 'day', other: 'days' }, short: 'd' }, - h: { long: { one: 'hour', other: 'hours' }, short: 'h' }, - m: { long: { one: 'minute', other: 'minutes' }, short: 'min' }, - s: { long: { one: 'second', other: 'seconds' }, short: 's' }, - }, - pl: { - y: { long: { one: 'rok', few: 'lata', many: 'lat', other: 'lat' }, short: 'r.' }, - M: { long: { one: 'miesiąc', few: 'miesiące', many: 'miesięcy', other: 'miesięcy' }, short: 'mies.' }, - d: { long: { one: 'dzień', few: 'dni', many: 'dni', other: 'dni' }, short: 'd' }, - h: { long: { one: 'godzina', few: 'godziny', many: 'godzin', other: 'godzin' }, short: 'godz.' }, - m: { long: { one: 'minuta', few: 'minuty', many: 'minut', other: 'minut' }, short: 'min' }, - s: { long: { one: 'sekunda', few: 'sekundy', many: 'sekund', other: 'sekund' }, short: 's' }, - } -}; -const clampLocale = loc => (LABELS[loc.slice(0, 2)] ? loc.slice(0, 2) : 'en'); - -/* ---------------- Diff with smart collapsing ---------------- - * We compute calendar years/months only if those units are shown. - * If not shown, their time "falls down" into lower units (days/hours/...). - */ -function extractYearsMonthsCalendar(a, b, wantY, wantM) { - let anchor = new Date(a); - let y = 0, M = 0; - - // Add full years while not passing end. - if (wantY) { - while (new Date( - anchor.getFullYear() + 1, anchor.getMonth(), anchor.getDate(), - anchor.getHours(), anchor.getMinutes(), anchor.getSeconds() - ) <= b) { anchor.setFullYear(anchor.getFullYear() + 1); y++; } - } - - // Then add full months while not passing end. - if (wantM) { - while (new Date( - anchor.getFullYear(), anchor.getMonth() + 1, anchor.getDate(), - anchor.getHours(), anchor.getMinutes(), anchor.getSeconds() - ) <= b) { anchor.setMonth(anchor.getMonth() + 1); M++; } - } - - return { y, M, anchor }; -} - -function collapseForUnits(start, end, show) { - const { y, M, anchor } = extractYearsMonthsCalendar(start, end, !!show.y, !!show.M); - - // If we showed Y/M, base the remaining ms on the anchor (after adding Y/M). - // Otherwise, consume everything from the start. - const base = (show.y || show.M) ? anchor : start; - let rest = Math.max(0, end - base); - - // Greedy consume into D/H/m/s only for units we plan to display. - const dayMs = 86400000, hourMs = 3600000, minMs = 60000, secMs = 1000; - let d = 0, h = 0, m = 0, s = 0; - - if (show.d) { d = Math.floor(rest / dayMs); rest -= d * dayMs; } - if (show.h) { h = Math.floor(rest / hourMs); rest -= h * hourMs; } - if (show.m) { m = Math.floor(rest / minMs); rest -= m * minMs; } - if (show.s) { s = Math.floor(rest / secMs); rest -= s * secMs; } - - return { years: y, months: M, days: d, hours: h, minutes: m, seconds: s }; -} - -/* ---------------- Formatters ---------------- - * pad2: always pad for H/M/S "digital" look. - * formatHMS: joins visible HMS with ':'. - * formatDurationSmart: uses DurationFormat if available; otherwise fallback. - */ -function pad2(n) { return String(n ?? 0).padStart(2, '0'); } - -function formatHMS(show, p) { - const segs = []; - if (show.h) segs.push(pad2(p.hours)); - if (show.m) segs.push(pad2(p.minutes)); - if (show.s) segs.push(pad2(p.seconds)); - return segs.join(':'); -} - -function formatDurationSmart(locale, labelsMode, show, parts) { - const lang2 = LABELS[locale.slice(0, 2)] ? locale.slice(0, 2) : 'en'; - - // labels="none" → head units as numbers, HMS as HH:MM:SS - if (labelsMode === 'none') { - const head = []; - if (show.y) head.push(String(parts.years)); - if (show.M) head.push(String(parts.months)); - if (show.d) head.push(String(parts.days)); - const tail = formatHMS(show, parts); - return [head.join(' '), tail].filter(Boolean).join(head.length && tail ? ' ' : ''); - } - - // Prefer native DurationFormat, it handles pluralization and spacing. - if (hasIDF) { - const style = labelsMode === 'short' ? 'short' : 'long'; - const opts = { style }; - if (show.h) opts.hours = '2-digit'; - if (show.m) opts.minutes = '2-digit'; - if (show.s) opts.seconds = '2-digit'; - - const df = new Intl.DurationFormat(locale, opts); - const inParts = {}; - if (show.y) inParts.years = parts.years; - if (show.M) inParts.months = parts.months; - if (show.d) inParts.days = parts.days; - if (show.h) inParts.hours = parts.hours; - if (show.m) inParts.minutes = parts.minutes; - if (show.s) inParts.seconds = parts.seconds; - return df.format(inParts); - } - - // Fallback: plural rules + our dictionaries - const PR = new Intl.PluralRules(locale); - const L = LABELS[lang2]; - - const head = []; - const push = (key, val) => { - const cat = PR.select(val); - const word = L[key].long[cat] ?? L[key].long.other; - head.push(labelsMode === 'short' ? `${val} ${L[key].short}` : `${val} ${word}`); - }; - if (show.y) push('y', parts.years); - if (show.M) push('M', parts.months); - if (show.d) push('d', parts.days); - - const hms = []; - const pushH = (key, val) => { - const cat = PR.select(val); - const word = L[key].long[cat] ?? L[key].long.other; - if (labelsMode === 'short') hms.push(`${pad2(val)} ${L[key].short}`); - else hms.push(`${pad2(val)} ${word}`); - }; - if (show.h) pushH('h', parts.hours); - if (show.m) pushH('m', parts.minutes); - if (show.s) pushH('s', parts.seconds); - - return [head.join(' '), hms.join(' ')].filter(Boolean).join(head.length && hms.length ? ' ' : ''); -} - -/* ---------------- Presets ---------------- - * hms → HH:MM:SS - * dhms → D + HH:MM:SS - * ydhms → Y + M + D + HH:MM:SS (calendar-aware) - * auto → HH:MM:SS when near; D + HH:MM:SS when far (>= 30 days) - * default → dhms - */ -function unitsForPreset(preset, start, end) { - if (preset === 'hms') return { y: false, M: false, d: false, h: true, m: true, s: true }; - if (preset === 'dhms') return { y: false, M: false, d: true, h: true, m: true, s: true }; - if (preset === 'ydhms') return { y: true, M: true, d: true, h: true, m: true, s: true }; - if (preset === 'auto') { - const diff = Math.max(0, end - start); - const dayMs = 86400000; - return diff >= 30 * dayMs - ? { y: false, M: false, d: true, h: true, m: true, s: true } - : { y: false, M: false, d: false, h: true, m: true, s: true }; - } - return { y: false, M: false, d: true, h: true, m: true, s: true }; -} - -/* ---------------- Web Component ---------------- - * Light-DOM: visible text is a single Text node inside . - * We never replace that node; we only update its .data → selection is stable. - */ -class WcTimer extends HTMLElement { - static get observedAttributes() { return ['target', 'preset', 'labels', 'locale', 'lang', 'compact']; } - - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - - // Shadow only defines style and a default slot. - // Text is in light DOM (selectable, styleable). - const style = document.createElement('style'); - style.textContent = ` - :host { - display:inline-block; - color: inherit; - user-select: text; -webkit-user-select: text; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; - font-variant-numeric: tabular-nums; - white-space: nowrap; - } - ::slotted(*) { all: unset; } - `; - const slot = document.createElement('slot'); - this.shadowRoot.append(style, slot); - - // Single managed text node in light DOM. - this._text = null; - - // Internal state. - this._unsub = null; - this._updatingAttr = false; // guard against attribute echo from setters - this._target = new Date(); - this._preset = 'auto'; - this._labels = 'short'; // 'none' | 'short' | 'long' - this._compact = false; - this._locale = 'en'; - } - - connectedCallback() { - // Ensure the first child is our Text node. - if (!this._text) { - if (this.firstChild && this.firstChild.nodeType === Node.TEXT_NODE) { - this._text = this.firstChild; - } else { - this.textContent = ''; - this._text = document.createTextNode(''); - this.appendChild(this._text); - } - } - - this._syncFromAttrs(); - this._renderOnce(Date.now()); - this._unsub = TickHub.sub(t => this._renderOnce(t)); - } - - disconnectedCallback() { - if (this._unsub) { this._unsub(); this._unsub = null; } - } - - attributeChangedCallback() { - // Ignore updates triggered by our own setters. - if (this._updatingAttr) return; - this._syncFromAttrs(); - this._renderOnce(Date.now()); - } - - // Pull current attributes into state. - _syncFromAttrs() { - const raw = this.getAttribute('target'); - const t = raw ? new Date(raw) : new Date(); - this._target = Number.isNaN(t.getTime()) ? new Date() : t; - - this._preset = (this.getAttribute('preset') || 'auto').toLowerCase(); // 'auto'|'hms'|'dhms'|'ydhms'|'date' - this._labels = (this.getAttribute('labels') || 'short').toLowerCase(); // 'none'|'short'|'long' - this._compact = this.hasAttribute('compact'); - - this._locale = resolveLocale(this); - } - - // Compute and render the string once. - _renderOnce(nowMs) { - if (!this._text) return; - - // Date-only preset: defer to Intl.DateTimeFormat. - if (this._preset === 'date') { - const dtf = new Intl.DateTimeFormat(this._locale, { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false - }); - const out = dtf.format(this._target); - if (this._text.data !== out) this._text.data = out; - return; - } - - // Build the time window depending on future/past. - const now = new Date(nowMs); - const future = this._target - now >= 0; - const start = future ? now : this._target; - const end = future ? this._target : now; - - // Decide what to show based on preset. - let show = unitsForPreset(this._preset, start, end); - - // compact: hide empty high units (Y/M/D), and 0 hours when days are shown. - if (this._compact) { - const probe = collapseForUnits(start, end, show); - if (show.y && probe.years === 0) show = { ...show, y: false }; - if (show.M && !show.y && probe.months === 0) show = { ...show, M: false }; - if (show.d && !show.y && !show.M && probe.days === 0) show = { ...show, d: false }; - const probe2 = collapseForUnits(start, end, show); - if (show.d && show.h && probe2.hours === 0) show = { ...show, h: false }; - } - - const parts = collapseForUnits(start, end, show); - - // If countdown reached zero, stop ticking to save CPU. - if (future && end - now <= 0 && this._unsub) { this._unsub(); this._unsub = null; } - - // Format the output string and update only TextNode.data. - const out = formatDurationSmart(this._locale, this._labels, show, parts); - if (this._text.data !== out) this._text.data = out; - } - - /* ======================= - * Public API (properties) - * ======================= - * Use these instead of touching private fields. - */ - - // target: Date | string | number(ms) - get target() { return this._target; } - set target(v) { - const d = this._coerceDate(v); - if (!d) return; - this._target = d; - this._updatingAttr = true; // prevent attributeChanged echo - this.setAttribute('target', d.toISOString()); - this._updatingAttr = false; - this._renderOnce(Date.now()); - } - - // preset: 'auto' | 'hms' | 'dhms' | 'ydhms' | 'date' - get preset() { return this._preset; } - set preset(v) { - const p = String(v || '').toLowerCase(); - this._preset = p; - this._updatingAttr = true; - this.setAttribute('preset', p); - this._updatingAttr = false; - this._renderOnce(Date.now()); - } - - // labels: 'none' | 'short' | 'long' - get labels() { return this._labels; } - set labels(v) { - const l = String(v || '').toLowerCase(); - this._labels = l; - this._updatingAttr = true; - this.setAttribute('labels', l); - this._updatingAttr = false; - this._renderOnce(Date.now()); - } - - // compact: boolean - get compact() { return this._compact; } - set compact(v) { - const on = !!v; - this._compact = on; - this._updatingAttr = true; - this.toggleAttribute('compact', on); - this._updatingAttr = false; - this._renderOnce(Date.now()); - } - - // locale: IETF BCP 47 (e.g., 'pl', 'en-GB'); overrides auto detection - get locale() { return this._locale; } - set locale(v) { - const loc = (v || '').toString(); - if (!loc) return; - this._locale = loc.toLowerCase(); - this._updatingAttr = true; - this.setAttribute('locale', this._locale); - this._updatingAttr = false; - this._renderOnce(Date.now()); - } - - // --- helpers --- - _coerceDate(v) { - if (v instanceof Date) return isNaN(v.getTime()) ? null : v; - if (typeof v === 'number') { const d = new Date(v); return isNaN(d.getTime()) ? null : d; } - if (typeof v === 'string') { const d = new Date(v); return isNaN(d.getTime()) ? null : d; } - return null; - } -} - -customElements.define('wc-timer', WcTimer);