|
1 | 1 | <script lang="ts"> |
2 | 2 | import { onMount } from "svelte"; |
3 | | - import { ScanCommand } from "@aws-sdk/client-dynamodb"; |
| 3 | + import { ReturnValue, ScanCommand, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; |
4 | 4 | import { initializeTooling, SessionState, State } from "$lib/state.js"; |
5 | 5 | import type { CNotification } from "$lib/types/notification.js"; |
6 | 6 | import { createModal } from "$lib/modal.js"; |
|
9 | 9 | let notifications: CNotification[] = $state([]); |
10 | 10 | let loading = $state(true); |
11 | 11 | let error: string | null = $state(null); |
12 | | - let filter: 'all' | 'active' | 'expired' = $state('all'); |
| 12 | + let filter: "all" | "active" | "expired" = $state("all"); |
13 | 13 |
|
14 | 14 | async function loadNotifications() { |
15 | 15 | try { |
|
22 | 22 |
|
23 | 23 | const table = "ccported_notifs"; |
24 | 24 | const params = { |
25 | | - TableName: table |
| 25 | + TableName: table, |
26 | 26 | }; |
27 | 27 |
|
28 | 28 | const command = new ScanCommand(params); |
29 | 29 | const data = await SessionState.dynamoDBClient?.send(command); |
30 | 30 |
|
31 | 31 | if (data && data.Items) { |
32 | | - notifications = data.Items.map(item => ({ |
| 32 | + notifications = data.Items.map((item) => ({ |
33 | 33 | notification_id: item.id?.S || "", |
34 | 34 | title: item.title?.S || "", |
35 | 35 | body: item.body?.S || "", |
36 | 36 | expires: parseInt(item.expires?.N || "0"), |
37 | 37 | ctaText: item.cta_text?.S, |
38 | | - ctaLink: item.cta_link?.S |
| 38 | + ctaLink: item.cta_link?.S, |
39 | 39 | })); |
40 | 40 |
|
41 | 41 | // Sort by expires descending (newest first) |
42 | 42 | notifications.sort((a, b) => b.expires - a.expires); |
43 | 43 | } |
44 | 44 | } catch (err) { |
45 | | - error = `Failed to load notifications: ${err instanceof Error ? err.message : 'Unknown error'}`; |
| 45 | + error = `Failed to load notifications: ${err instanceof Error ? err.message : "Unknown error"}`; |
46 | 46 | } finally { |
47 | 47 | loading = false; |
48 | 48 | } |
49 | 49 | } |
50 | 50 |
|
51 | 51 | function getFilteredNotifications() { |
52 | 52 | const now = Date.now(); |
53 | | - return notifications.filter(notif => { |
54 | | - if (filter === 'active') return notif.expires > now; |
55 | | - if (filter === 'expired') return notif.expires <= now; |
| 53 | + return notifications.filter((notif) => { |
| 54 | + if (filter === "active") return notif.expires > now; |
| 55 | + if (filter === "expired") return notif.expires <= now; |
56 | 56 | return true; // 'all' |
57 | 57 | }); |
58 | 58 | } |
|
71 | 71 | content: notif.body, |
72 | 72 | actions: [ |
73 | 73 | ...(notif.ctaText && notif.ctaLink |
74 | | - ? [{ |
75 | | - label: notif.ctaText, |
76 | | - onClick: () => window.open(notif.ctaLink, "_blank") |
77 | | - }] |
| 74 | + ? [ |
| 75 | + { |
| 76 | + label: notif.ctaText, |
| 77 | + onClick: () => |
| 78 | + window.open(notif.ctaLink, "_blank"), |
| 79 | + }, |
| 80 | + ] |
78 | 81 | : []), |
79 | 82 | { label: "Close", onClick: (modal) => modal.close() }, |
80 | 83 | ], |
|
84 | 87 | function markAsSeen(notificationId: string) { |
85 | 88 | if (!State.seenNotifications.includes(notificationId)) { |
86 | 89 | State.seenNotifications.push(notificationId); |
| 90 | + addImpression(notificationId); |
87 | 91 | } |
88 | 92 | } |
| 93 | + function addImpression(notification_id: string) { |
| 94 | + const params = { |
| 95 | + TableName: "ccported_notifs", |
| 96 | + Key: { |
| 97 | + notification_id: { S: notification_id }, |
| 98 | + }, |
| 99 | + UpdateExpression: "ADD impressions :inc", |
| 100 | + ExpressionAttributeValues: { |
| 101 | + ":inc": { N: "1" }, |
| 102 | + }, |
| 103 | + ReturnValues: ReturnValue.UPDATED_NEW, |
| 104 | + }; |
| 105 | + const updateCommand = new UpdateItemCommand(params); |
| 106 | + SessionState.dynamoDBClient?.send(updateCommand).catch(console.error); |
| 107 | + } |
89 | 108 |
|
90 | 109 | onMount(() => { |
91 | 110 | loadNotifications(); |
|
94 | 113 |
|
95 | 114 | <svelte:head> |
96 | 115 | <title>Notifications - CCPorted</title> |
97 | | - <meta name="description" content="View all notifications and announcements from CCPorted" /> |
| 116 | + <meta |
| 117 | + name="description" |
| 118 | + content="View all notifications and announcements from CCPorted" |
| 119 | + /> |
98 | 120 | </svelte:head> |
99 | 121 |
|
100 | 122 | <Navigation /> |
|
103 | 125 | <div class="container"> |
104 | 126 | <div class="header"> |
105 | 127 | <h1>🔔 Notifications</h1> |
106 | | - <p>Stay updated with the latest announcements and news from CCPorted</p> |
| 128 | + <p> |
| 129 | + Stay updated with the latest announcements and news from |
| 130 | + CCPorted |
| 131 | + </p> |
107 | 132 | </div> |
108 | 133 |
|
109 | 134 | <div class="controls"> |
110 | 135 | <div class="filter-tabs"> |
111 | | - <button |
112 | | - class="tab" |
113 | | - class:active={filter === 'all'} |
114 | | - onclick={() => filter = 'all'} |
| 136 | + <button |
| 137 | + class="tab" |
| 138 | + class:active={filter === "all"} |
| 139 | + onclick={() => (filter = "all")} |
115 | 140 | > |
116 | 141 | All ({notifications.length}) |
117 | 142 | </button> |
118 | | - <button |
119 | | - class="tab" |
120 | | - class:active={filter === 'active'} |
121 | | - onclick={() => filter = 'active'} |
| 143 | + <button |
| 144 | + class="tab" |
| 145 | + class:active={filter === "active"} |
| 146 | + onclick={() => (filter = "active")} |
122 | 147 | > |
123 | | - Active ({notifications.filter(n => !isExpired(n.expires)).length}) |
| 148 | + Active ({notifications.filter((n) => !isExpired(n.expires)) |
| 149 | + .length}) |
124 | 150 | </button> |
125 | | - <button |
126 | | - class="tab" |
127 | | - class:active={filter === 'expired'} |
128 | | - onclick={() => filter = 'expired'} |
| 151 | + <button |
| 152 | + class="tab" |
| 153 | + class:active={filter === "expired"} |
| 154 | + onclick={() => (filter = "expired")} |
129 | 155 | > |
130 | | - Past ({notifications.filter(n => isExpired(n.expires)).length}) |
| 156 | + Past ({notifications.filter((n) => isExpired(n.expires)) |
| 157 | + .length}) |
131 | 158 | </button> |
132 | 159 | </div> |
133 | 160 | <button class="refresh-btn" onclick={loadNotifications}> |
134 | | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| 161 | + <svg |
| 162 | + width="16" |
| 163 | + height="16" |
| 164 | + viewBox="0 0 24 24" |
| 165 | + fill="none" |
| 166 | + stroke="currentColor" |
| 167 | + stroke-width="2" |
| 168 | + > |
135 | 169 | <polyline points="23 4 23 10 17 10"></polyline> |
136 | 170 | <polyline points="1 20 1 14 7 14"></polyline> |
137 | | - <path d="m20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path> |
| 171 | + <path |
| 172 | + d="m20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" |
| 173 | + ></path> |
138 | 174 | </svg> |
139 | 175 | Refresh |
140 | 176 | </button> |
|
155 | 191 | <div class="notifications-list"> |
156 | 192 | {#each getFilteredNotifications() as notif (notif.notification_id)} |
157 | 193 | {@const expired = isExpired(notif.expires)} |
158 | | - {@const seen = State.seenNotifications.includes(notif.notification_id)} |
| 194 | + {@const seen = State.seenNotifications.includes( |
| 195 | + notif.notification_id, |
| 196 | + )} |
159 | 197 | <!-- svelte-ignore a11y_click_events_have_key_events --> |
160 | 198 | <!-- svelte-ignore a11y_no_static_element_interactions --> |
161 | | - <div |
162 | | - class="notification-card" |
163 | | - class:expired |
| 199 | + <div |
| 200 | + class="notification-card" |
| 201 | + class:expired |
164 | 202 | class:unseen={!seen} |
165 | 203 | onclick={() => { |
166 | 204 | viewNotification(notif); |
|
171 | 209 | <h3>{notif.title}</h3> |
172 | 210 | <div class="notification-meta"> |
173 | 211 | <span class="status" class:expired> |
174 | | - {expired ? '📋 Past' : '🔴 Active'} |
| 212 | + {expired ? "📋 Past" : "🔴 Active"} |
175 | 213 | </span> |
176 | 214 | {#if !seen && !expired} |
177 | 215 | <span class="new-badge">NEW</span> |
178 | 216 | {/if} |
179 | 217 | </div> |
180 | 218 | </div> |
181 | | - |
| 219 | + |
182 | 220 | <div class="notification-body"> |
183 | 221 | <p>{@html notif.body}</p> |
184 | 222 | </div> |
185 | | - |
| 223 | + |
186 | 224 | <div class="notification-footer"> |
187 | 225 | <div class="expires"> |
188 | | - {expired ? 'Expired' : 'Expires'}: {formatDate(notif.expires)} |
| 226 | + {expired ? "Expired" : "Expires"}: {formatDate( |
| 227 | + notif.expires, |
| 228 | + )} |
189 | 229 | </div> |
190 | 230 | {#if notif.ctaText && notif.ctaLink} |
191 | 231 | <div class="cta-info"> |
192 | | - <span class="cta-label">Action: {notif.ctaText}</span> |
| 232 | + <span class="cta-label" |
| 233 | + >Action: {notif.ctaText}</span |
| 234 | + > |
193 | 235 | </div> |
194 | 236 | {/if} |
195 | 237 | </div> |
|
201 | 243 | <div class="empty-icon">📭</div> |
202 | 244 | <h3>No notifications found</h3> |
203 | 245 | <p> |
204 | | - {#if filter === 'active'} |
| 246 | + {#if filter === "active"} |
205 | 247 | There are no active notifications at the moment. |
206 | | - {:else if filter === 'expired'} |
| 248 | + {:else if filter === "expired"} |
207 | 249 | No past notifications to display. |
208 | 250 | {:else} |
209 | 251 | No notifications available. |
|
332 | 374 | } |
333 | 375 |
|
334 | 376 | @keyframes spin { |
335 | | - 0% { transform: rotate(0deg); } |
336 | | - 100% { transform: rotate(360deg); } |
| 377 | + 0% { |
| 378 | + transform: rotate(0deg); |
| 379 | + } |
| 380 | + 100% { |
| 381 | + transform: rotate(360deg); |
| 382 | + } |
337 | 383 | } |
338 | 384 |
|
339 | 385 | .error { |
|
448 | 494 | } |
449 | 495 |
|
450 | 496 | @keyframes pulse { |
451 | | - 0%, 100% { opacity: 1; } |
452 | | - 50% { opacity: 0.7; } |
| 497 | + 0%, |
| 498 | + 100% { |
| 499 | + opacity: 1; |
| 500 | + } |
| 501 | + 50% { |
| 502 | + opacity: 0.7; |
| 503 | + } |
453 | 504 | } |
454 | 505 |
|
455 | 506 | .notification-body p { |
|
0 commit comments