From c9880baddba3c343dcb78057d8901148722ec14c Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Sun, 29 Mar 2026 10:22:58 +0200 Subject: [PATCH] feat: add Stimulus notifications controller with polling --- .../controllers/notifications_controller.js | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 assets/controllers/notifications_controller.js diff --git a/assets/controllers/notifications_controller.js b/assets/controllers/notifications_controller.js new file mode 100644 index 0000000..cfb31a3 --- /dev/null +++ b/assets/controllers/notifications_controller.js @@ -0,0 +1,77 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['badge', 'list']; + + connect() { + this._poll(); + this._interval = setInterval(() => this._poll(), 30000); + } + + disconnect() { + clearInterval(this._interval); + } + + async _poll() { + try { + const response = await fetch('/api/notifications'); + if (!response.ok) return; + + const data = await response.json(); + this._updateBadge(data.unreadCount); + this._updateTitle(data.unreadCount); + this._updateList(data.notifications); + } catch (e) { + // silently ignore polling errors + } + } + + _updateBadge(count) { + if (count > 0) { + this.badgeTarget.textContent = count; + this.badgeTarget.hidden = false; + } else { + this.badgeTarget.hidden = true; + } + } + + _updateTitle(count) { + const base = 'Actorle'; + document.title = count > 0 ? `(${count}) ${base}` : base; + } + + _updateList(notifications) { + if (notifications.length === 0) { + this.listTarget.innerHTML = ''; + return; + } + + this.listTarget.innerHTML = notifications.map(n => ` +
+

${this._escapeHtml(n.message)}

+ +
+ `).join(''); + } + + async markRead() { + await fetch('/api/notifications/read', { method: 'POST' }); + this._poll(); + } + + _escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + _formatDate(isoString) { + const date = new Date(isoString); + return date.toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'short', + hour: '2-digit', + minute: '2-digit', + }); + } +}