| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- import Alpine from 'alpinejs';
- import 'htmx.org';
- import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip, Title } from 'chart.js';
- // Dark mode toggle. Layout's inline <head> script handles the FOUC-free
- // initial paint; this just wires the toggle button.
- function applyTheme(theme) {
- if (theme === 'dark') {
- document.documentElement.classList.add('dark');
- } else {
- document.documentElement.classList.remove('dark');
- }
- try {
- localStorage.setItem('irdb-theme', theme);
- } catch (e) {
- /* ignore */
- }
- }
- document.addEventListener('click', (e) => {
- const target = e.target.closest('[data-theme-toggle]');
- if (!target) return;
- const next = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
- applyTheme(next);
- });
- // htmx: send the per-session CSRF token on every state-changing request.
- document.body.addEventListener('htmx:configRequest', (e) => {
- const meta = document.querySelector('meta[name="csrf-token"]');
- if (meta && meta.content) {
- e.detail.headers['X-CSRF-Token'] = meta.content;
- }
- });
- // Dashboard reports-per-hour chart. The canvas carries the buckets in a
- // `data-buckets` attribute (server-pre-bucketed; no AJAX). Chart.js is
- // tree-shaken to just the bar/linear pieces we need so the bundle stays
- // small.
- Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Title);
- function renderReportsChart() {
- const canvas = document.getElementById('reports-chart');
- if (!canvas) return;
- let buckets = [];
- try {
- buckets = JSON.parse(canvas.dataset.buckets || '[]');
- } catch (e) {
- return;
- }
- const labels = buckets.map((b) => (b.hour || '').replace(/.*T(\d{2}).*/, '$1h'));
- const data = buckets.map((b) => b.count || 0);
- const isDark = document.documentElement.classList.contains('dark');
- const tickColor = isDark ? '#94a3b8' : '#475569';
- const gridColor = isDark ? 'rgba(148,163,184,0.15)' : 'rgba(148,163,184,0.3)';
- new Chart(canvas, {
- type: 'bar',
- data: {
- labels,
- datasets: [{
- label: 'reports',
- data,
- backgroundColor: '#6366f1',
- }],
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- scales: {
- x: { ticks: { color: tickColor }, grid: { color: gridColor } },
- y: { ticks: { color: tickColor, precision: 0 }, grid: { color: gridColor }, beginAtZero: true },
- },
- },
- });
- }
- document.addEventListener('DOMContentLoaded', renderReportsChart);
- // Locale-aware <time> rendering. Templates emit `<time class="irdb-dt"
- // datetime="<iso>">…</iso></time>`; the text content holds the raw ISO
- // string as a no-JS fallback. This pass replaces it with the user's
- // browser locale formatting, with an optional configured fallback (set
- // via UI_LOCALE on the html data attribute) appended so browser locale
- // wins but a deployment can still ensure something sensible if the
- // browser's preference isn't supported. `data-irdb-dt-format="date"`
- // switches to date-only output.
- function getDateLocales() {
- const fallback = document.documentElement.getAttribute('data-irdb-locale-fallback');
- const locales = [];
- if (typeof navigator !== 'undefined' && navigator.language) {
- locales.push(navigator.language);
- }
- if (fallback && fallback.trim()) {
- locales.push(fallback.trim());
- }
- return locales.length > 0 ? locales : undefined;
- }
- function buildDateFormatters() {
- const locales = getDateLocales();
- return {
- datetime: new Intl.DateTimeFormat(locales, {
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit',
- }),
- date: new Intl.DateTimeFormat(locales, {
- year: 'numeric', month: '2-digit', day: '2-digit',
- }),
- };
- }
- function formatTimes(root) {
- const scope = root && root.querySelectorAll ? root : document;
- const formatters = buildDateFormatters();
- const elements = scope.querySelectorAll('time.irdb-dt[datetime]');
- elements.forEach((el) => {
- const iso = el.getAttribute('datetime');
- if (!iso) return;
- const d = new Date(iso);
- if (isNaN(d.getTime())) return;
- const fmt = el.dataset.irdbDtFormat === 'date' ? formatters.date : formatters.datetime;
- try {
- el.textContent = fmt.format(d);
- if (!el.hasAttribute('title')) {
- el.setAttribute('title', iso);
- }
- } catch (e) {
- /* leave the ISO fallback in place */
- }
- });
- }
- document.addEventListener('DOMContentLoaded', () => formatTimes(document));
- document.body.addEventListener('htmx:afterSettle', (e) => formatTimes(e.target));
- window.Alpine = Alpine;
- Alpine.start();
|