Skip to content

Commit 1bf71ee

Browse files
committed
add notification impression tracking + actual mark as read
1 parent 07810dc commit 1bf71ee

3 files changed

Lines changed: 118 additions & 50 deletions

File tree

src/lib/checkNotifications.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ScanCommand } from "@aws-sdk/client-dynamodb";
1+
import { ReturnConsumedCapacity, ReturnValue, ScanCommand, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
22
import { initializeTooling, SessionState, State } from "./state.js";
33
import type { CNotification } from "./types/notification.js";
4-
import { createModal } from "./modal.js"; // Assume you have a modal utility
4+
import { createModal } from "./modal.js";
55

66
export async function checkNotifications() {
77

@@ -31,7 +31,7 @@ export async function checkNotifications() {
3131
if (data && data.Items && data.Items.length > 0) {
3232
const notifications = data.Items.map(item => {
3333
return {
34-
notification_id: item.id?.S || "",
34+
notification_id: item.notification_id?.S || "",
3535
title: item.title?.S || "",
3636
body: item.body?.S || "",
3737
expires: parseInt(item.expires?.N || "0"),
@@ -55,6 +55,7 @@ function handleNotifs(notifications: CNotification[]) {
5555
return; // Already seen
5656
}
5757
State.seenNotifications.push(notif.notification_id);
58+
addImpression(notif.notification_id);
5859
createModal({
5960
title: notif.title,
6061
content: notif.body,
@@ -69,4 +70,20 @@ function handleNotifs(notifications: CNotification[]) {
6970
],
7071
});
7172
});
73+
}
74+
75+
function addImpression(notification_id: string) {
76+
const params = {
77+
TableName: "ccported_notifs",
78+
Key: {
79+
notification_id: { S: notification_id }
80+
},
81+
UpdateExpression: "ADD impressions :inc",
82+
ExpressionAttributeValues: {
83+
":inc": { N: "1" }
84+
},
85+
ReturnValues: ReturnValue.UPDATED_NEW
86+
}
87+
const updateCommand = new UpdateItemCommand(params);
88+
SessionState.dynamoDBClient?.send(updateCommand).catch(console.error);
7289
}

src/routes/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
onMount(async () => {
1616
await initializeTooling();
1717
18-
// Dont wait up on this one (it can load in the background)
18+
// Don't wait up on this one (it can load in the background)
1919
checkNotifications();
2020
isAHost = State.isAHost();
2121

src/routes/notifications/+page.svelte

Lines changed: 97 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { onMount } from "svelte";
3-
import { ScanCommand } from "@aws-sdk/client-dynamodb";
3+
import { ReturnValue, ScanCommand, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
44
import { initializeTooling, SessionState, State } from "$lib/state.js";
55
import type { CNotification } from "$lib/types/notification.js";
66
import { createModal } from "$lib/modal.js";
@@ -9,7 +9,7 @@
99
let notifications: CNotification[] = $state([]);
1010
let loading = $state(true);
1111
let error: string | null = $state(null);
12-
let filter: 'all' | 'active' | 'expired' = $state('all');
12+
let filter: "all" | "active" | "expired" = $state("all");
1313
1414
async function loadNotifications() {
1515
try {
@@ -22,37 +22,37 @@
2222
2323
const table = "ccported_notifs";
2424
const params = {
25-
TableName: table
25+
TableName: table,
2626
};
2727
2828
const command = new ScanCommand(params);
2929
const data = await SessionState.dynamoDBClient?.send(command);
3030
3131
if (data && data.Items) {
32-
notifications = data.Items.map(item => ({
32+
notifications = data.Items.map((item) => ({
3333
notification_id: item.id?.S || "",
3434
title: item.title?.S || "",
3535
body: item.body?.S || "",
3636
expires: parseInt(item.expires?.N || "0"),
3737
ctaText: item.cta_text?.S,
38-
ctaLink: item.cta_link?.S
38+
ctaLink: item.cta_link?.S,
3939
}));
4040
4141
// Sort by expires descending (newest first)
4242
notifications.sort((a, b) => b.expires - a.expires);
4343
}
4444
} 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"}`;
4646
} finally {
4747
loading = false;
4848
}
4949
}
5050
5151
function getFilteredNotifications() {
5252
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;
5656
return true; // 'all'
5757
});
5858
}
@@ -71,10 +71,13 @@
7171
content: notif.body,
7272
actions: [
7373
...(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+
]
7881
: []),
7982
{ label: "Close", onClick: (modal) => modal.close() },
8083
],
@@ -84,8 +87,24 @@
8487
function markAsSeen(notificationId: string) {
8588
if (!State.seenNotifications.includes(notificationId)) {
8689
State.seenNotifications.push(notificationId);
90+
addImpression(notificationId);
8791
}
8892
}
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+
}
89108
90109
onMount(() => {
91110
loadNotifications();
@@ -94,7 +113,10 @@
94113

95114
<svelte:head>
96115
<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+
/>
98120
</svelte:head>
99121

100122
<Navigation />
@@ -103,38 +125,52 @@
103125
<div class="container">
104126
<div class="header">
105127
<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>
107132
</div>
108133

109134
<div class="controls">
110135
<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")}
115140
>
116141
All ({notifications.length})
117142
</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")}
122147
>
123-
Active ({notifications.filter(n => !isExpired(n.expires)).length})
148+
Active ({notifications.filter((n) => !isExpired(n.expires))
149+
.length})
124150
</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")}
129155
>
130-
Past ({notifications.filter(n => isExpired(n.expires)).length})
156+
Past ({notifications.filter((n) => isExpired(n.expires))
157+
.length})
131158
</button>
132159
</div>
133160
<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+
>
135169
<polyline points="23 4 23 10 17 10"></polyline>
136170
<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>
138174
</svg>
139175
Refresh
140176
</button>
@@ -155,12 +191,14 @@
155191
<div class="notifications-list">
156192
{#each getFilteredNotifications() as notif (notif.notification_id)}
157193
{@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+
)}
159197
<!-- svelte-ignore a11y_click_events_have_key_events -->
160198
<!-- 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
164202
class:unseen={!seen}
165203
onclick={() => {
166204
viewNotification(notif);
@@ -171,25 +209,29 @@
171209
<h3>{notif.title}</h3>
172210
<div class="notification-meta">
173211
<span class="status" class:expired>
174-
{expired ? '📋 Past' : '🔴 Active'}
212+
{expired ? "📋 Past" : "🔴 Active"}
175213
</span>
176214
{#if !seen && !expired}
177215
<span class="new-badge">NEW</span>
178216
{/if}
179217
</div>
180218
</div>
181-
219+
182220
<div class="notification-body">
183221
<p>{@html notif.body}</p>
184222
</div>
185-
223+
186224
<div class="notification-footer">
187225
<div class="expires">
188-
{expired ? 'Expired' : 'Expires'}: {formatDate(notif.expires)}
226+
{expired ? "Expired" : "Expires"}: {formatDate(
227+
notif.expires,
228+
)}
189229
</div>
190230
{#if notif.ctaText && notif.ctaLink}
191231
<div class="cta-info">
192-
<span class="cta-label">Action: {notif.ctaText}</span>
232+
<span class="cta-label"
233+
>Action: {notif.ctaText}</span
234+
>
193235
</div>
194236
{/if}
195237
</div>
@@ -201,9 +243,9 @@
201243
<div class="empty-icon">📭</div>
202244
<h3>No notifications found</h3>
203245
<p>
204-
{#if filter === 'active'}
246+
{#if filter === "active"}
205247
There are no active notifications at the moment.
206-
{:else if filter === 'expired'}
248+
{:else if filter === "expired"}
207249
No past notifications to display.
208250
{:else}
209251
No notifications available.
@@ -332,8 +374,12 @@
332374
}
333375
334376
@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+
}
337383
}
338384
339385
.error {
@@ -448,8 +494,13 @@
448494
}
449495
450496
@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+
}
453504
}
454505
455506
.notification-body p {

0 commit comments

Comments
 (0)