Rewild Your Web
web browser dweb
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 508943f815334a02aefc47d9da365aba115ab0f5 207 lines 5.1 kB view raw
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);