Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import {
4 LitElement,
5 html,
6 css,
7} from "//shared.localhost:8888/third_party/lit/lit-all.min.js";
8
9export class NotificationPanel extends LitElement {
10 static properties = {
11 open: { type: Boolean, reflect: true },
12 notifications: { type: Array, state: true },
13 };
14
15 static styles = css`
16 @import url("//system.localhost:8888/notification_panel.css");
17 `;
18
19 constructor() {
20 super();
21 this.open = false;
22 this.notifications = [];
23 this.handleKeyDown = this.handleKeyDown.bind(this);
24 }
25
26 connectedCallback() {
27 super.connectedCallback();
28 }
29
30 disconnectedCallback() {
31 super.disconnectedCallback();
32 this.removeEventListeners();
33 }
34
35 updated(changedProperties) {
36 if (changedProperties.has("open")) {
37 if (this.open) {
38 requestAnimationFrame(() => {
39 document.addEventListener("keydown", this.handleKeyDown);
40 });
41 } else {
42 this.removeEventListeners();
43 }
44 }
45 }
46
47 removeEventListeners() {
48 document.removeEventListener("keydown", this.handleKeyDown);
49 }
50
51 handleKeyDown(e) {
52 if (e.key === "Escape") {
53 this.close();
54 }
55 }
56
57 close() {
58 this.open = false;
59 this.dispatchEvent(
60 new CustomEvent("panel-closed", {
61 bubbles: true,
62 composed: true,
63 })
64 );
65 }
66
67 handleBackdropClick(e) {
68 if (e.target.classList.contains("backdrop")) {
69 this.close();
70 }
71 }
72
73 handleNotificationClick(notification, e) {
74 // Don't handle click if dismiss button was clicked
75 if (e.target.closest(".notification-dismiss")) {
76 return;
77 }
78
79 this.dispatchEvent(
80 new CustomEvent("notification-click", {
81 bubbles: true,
82 composed: true,
83 detail: { notification },
84 })
85 );
86 }
87
88 handleDismiss(notification, e) {
89 e.stopPropagation();
90 this.dispatchEvent(
91 new CustomEvent("notification-dismiss", {
92 bubbles: true,
93 composed: true,
94 detail: { notification },
95 })
96 );
97 }
98
99 handleClearAll() {
100 this.dispatchEvent(
101 new CustomEvent("notification-clear-all", {
102 bubbles: true,
103 composed: true,
104 })
105 );
106 }
107
108 formatTimeAgo(timestamp) {
109 if (!timestamp) {
110 return "";
111 }
112
113 const now = Date.now();
114 const diff = now - timestamp;
115
116 const seconds = Math.floor(diff / 1000);
117 const minutes = Math.floor(seconds / 60);
118 const hours = Math.floor(minutes / 60);
119 const days = Math.floor(hours / 24);
120
121 if (days > 0) {
122 return `${days}d ago`;
123 }
124 if (hours > 0) {
125 return `${hours}h ago`;
126 }
127 if (minutes > 0) {
128 return `${minutes}m ago`;
129 }
130 return "Just now";
131 }
132
133 renderNotificationIcon(notification) {
134 if (notification.iconUrl) {
135 return html`<img src="${notification.iconUrl}" alt="" />`;
136 }
137 return html`<lucide-icon name="bell"></lucide-icon>`;
138 }
139
140 renderNotification(notification) {
141 return html`
142 <div
143 class="notification-item"
144 @click=${(e) => this.handleNotificationClick(notification, e)}
145 >
146 <div class="notification-header">
147 <div class="notification-icon">
148 ${this.renderNotificationIcon(notification)}
149 </div>
150 <div class="notification-content">
151 <div class="notification-title">${notification.title}</div>
152 <div class="notification-body">${notification.body}</div>
153 <div class="notification-meta">
154 <span class="notification-time"
155 >${this.formatTimeAgo(notification.timestamp)}</span
156 >
157 </div>
158 </div>
159 </div>
160 <button
161 class="notification-dismiss"
162 @click=${(e) => this.handleDismiss(notification, e)}
163 title="Dismiss"
164 >
165 <lucide-icon name="x" size="14"></lucide-icon>
166 </button>
167 </div>
168 `;
169 }
170
171 renderEmptyState() {
172 return html`
173 <div class="empty-state">
174 <lucide-icon name="bell-off"></lucide-icon>
175 <div class="empty-state-text">No notifications</div>
176 </div>
177 `;
178 }
179
180 render() {
181 return html`
182 <div class="backdrop" @click=${this.handleBackdropClick}></div>
183 <div class="panel">
184 <div class="header">
185 <span class="header-title">Notifications</span>
186 <div class="header-actions">
187 ${this.notifications.length > 0
188 ? html`<button class="clear-btn" @click=${this.handleClearAll}>
189 Clear all
190 </button>`
191 : ""}
192 <button class="close-btn" @click=${() => this.close()}>
193 <lucide-icon name="x" size="16"></lucide-icon>
194 </button>
195 </div>
196 </div>
197 <div class="notification-list">
198 ${this.notifications.length > 0
199 ? this.notifications.map((n) => this.renderNotification(n))
200 : this.renderEmptyState()}
201 </div>
202 </div>
203 `;
204 }
205}
206
207customElements.define("notification-panel", NotificationPanel);