app.js 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import Alpine from 'alpinejs';
  2. import 'htmx.org';
  3. import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip, Title } from 'chart.js';
  4. // Dark mode toggle. Layout's inline <head> script handles the FOUC-free
  5. // initial paint; this just wires the toggle button.
  6. function applyTheme(theme) {
  7. if (theme === 'dark') {
  8. document.documentElement.classList.add('dark');
  9. } else {
  10. document.documentElement.classList.remove('dark');
  11. }
  12. try {
  13. localStorage.setItem('irdb-theme', theme);
  14. } catch (e) {
  15. /* ignore */
  16. }
  17. }
  18. document.addEventListener('click', (e) => {
  19. const target = e.target.closest('[data-theme-toggle]');
  20. if (!target) return;
  21. const next = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
  22. applyTheme(next);
  23. });
  24. // htmx: send the per-session CSRF token on every state-changing request.
  25. document.body.addEventListener('htmx:configRequest', (e) => {
  26. const meta = document.querySelector('meta[name="csrf-token"]');
  27. if (meta && meta.content) {
  28. e.detail.headers['X-CSRF-Token'] = meta.content;
  29. }
  30. });
  31. // Dashboard reports-per-hour chart. The canvas carries the buckets in a
  32. // `data-buckets` attribute (server-pre-bucketed; no AJAX). Chart.js is
  33. // tree-shaken to just the bar/linear pieces we need so the bundle stays
  34. // small.
  35. Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Title);
  36. function renderReportsChart() {
  37. const canvas = document.getElementById('reports-chart');
  38. if (!canvas) return;
  39. let buckets = [];
  40. try {
  41. buckets = JSON.parse(canvas.dataset.buckets || '[]');
  42. } catch (e) {
  43. return;
  44. }
  45. const labels = buckets.map((b) => (b.hour || '').replace(/.*T(\d{2}).*/, '$1h'));
  46. const data = buckets.map((b) => b.count || 0);
  47. const isDark = document.documentElement.classList.contains('dark');
  48. const tickColor = isDark ? '#94a3b8' : '#475569';
  49. const gridColor = isDark ? 'rgba(148,163,184,0.15)' : 'rgba(148,163,184,0.3)';
  50. new Chart(canvas, {
  51. type: 'bar',
  52. data: {
  53. labels,
  54. datasets: [{
  55. label: 'reports',
  56. data,
  57. backgroundColor: '#6366f1',
  58. }],
  59. },
  60. options: {
  61. responsive: true,
  62. maintainAspectRatio: false,
  63. plugins: { legend: { display: false } },
  64. scales: {
  65. x: { ticks: { color: tickColor }, grid: { color: gridColor } },
  66. y: { ticks: { color: tickColor, precision: 0 }, grid: { color: gridColor }, beginAtZero: true },
  67. },
  68. },
  69. });
  70. }
  71. document.addEventListener('DOMContentLoaded', renderReportsChart);
  72. window.Alpine = Alpine;
  73. Alpine.start();