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);