add administrator notification bell
This commit is contained in:
+28
-1
@@ -16,6 +16,8 @@
|
||||
let realtimeState = 'DISCONNECTED';
|
||||
let realtimeSocket: WebSocket | undefined;
|
||||
let activityEvents: Array<{ event_id: string; event_type: string; occurred_at: string }> = [];
|
||||
let notifications: Array<{ id: string; severity: string; title: string; message: string; seen_at: string | null; created_at: string }> = [];
|
||||
let showNotifications = false;
|
||||
let showStreamEditor = false;
|
||||
let streamBusy = false;
|
||||
let streamError = '';
|
||||
@@ -89,6 +91,7 @@
|
||||
mustReplaceCredentials = result.must_replace_credentials;
|
||||
connectionState = 'CONNECTED';
|
||||
await loadActivity();
|
||||
await loadNotifications();
|
||||
await loadStreamInfo();
|
||||
connectRealtime();
|
||||
if (!mustReplaceCredentials) password = '';
|
||||
@@ -136,6 +139,8 @@
|
||||
authenticated = false;
|
||||
mustReplaceCredentials = false;
|
||||
oauthLink = '';
|
||||
notifications = [];
|
||||
showNotifications = false;
|
||||
}
|
||||
|
||||
function connectRealtime() {
|
||||
@@ -182,6 +187,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotifications() {
|
||||
try {
|
||||
const response = await fetch(`${serverAddress}/api/v1/admin/notifications`, {
|
||||
headers: { authorization: `Bearer ${sessionToken}` },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
notifications = await response.json();
|
||||
} catch {
|
||||
notifications = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function markNotificationSeen(notificationId: string) {
|
||||
const response = await fetch(`${serverAddress}/api/v1/admin/notifications/${notificationId}/seen`, {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${sessionToken}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
notifications = notifications.map((notification) => notification.id === notificationId ? { ...notification, seen_at: new Date().toISOString() } : notification);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStreamInfo() {
|
||||
try {
|
||||
const response = await fetch(`${serverAddress}/api/v1/twitch/stream-info`, {
|
||||
@@ -319,7 +346,7 @@
|
||||
<button class:active={activeView === item} class="nav-item" onclick={() => (activeView = item)}>{item}</button>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="app-actions"><span class:online={connectionState === 'CONNECTED'} class="connection-state">{connectionState}</span>{#if connectionNotice}<span class="connection-notice">{connectionNotice}</span>{/if}<button class="icon-button" aria-label="Settings">⚙</button></div>
|
||||
<div class="app-actions"><span class:online={connectionState === 'CONNECTED'} class="connection-state">{connectionState}</span>{#if connectionNotice}<span class="connection-notice">{connectionNotice}</span>{/if}{#if authenticated}<div class="notification-wrap"><button class="icon-button notification-button" aria-label="Notifications" onclick={() => (showNotifications = !showNotifications)}>♢{#if notifications.some((notification) => !notification.seen_at)}<span class="notification-dot"></span>{/if}</button>{#if showNotifications}<div class="notification-popover"><div class="notification-heading"><span>NOTIFICATIONS</span><button class="secondary" onclick={loadNotifications}>↻</button></div>{#if notifications.length}{#each notifications as notification}<div class:unseen={!notification.seen_at} class="notification-item"><div class="notification-title"><span>{notification.title}</span><span class="micro">{notification.severity}</span></div><div class="notification-message">{notification.message}</div>{#if !notification.seen_at}<button class="secondary-action" onclick={() => markNotificationSeen(notification.id)}>MARK SEEN</button>{/if}</div>{/each}{:else}<div class="notification-empty">No notifications.</div>{/if}</div>{/if}</div>{/if}<button class="icon-button" aria-label="Settings">⚙</button></div>
|
||||
</header>
|
||||
|
||||
<main class="workspace">
|
||||
|
||||
@@ -174,6 +174,70 @@ button:hover:not(:disabled) {
|
||||
color: var(--ed-dim);
|
||||
font-size: 18px;
|
||||
}
|
||||
.notification-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.notification-button {
|
||||
position: relative;
|
||||
}
|
||||
.notification-dot {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--ed-warn);
|
||||
}
|
||||
.notification-popover {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 34px;
|
||||
right: 0;
|
||||
width: 320px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--ui-line);
|
||||
background: var(--ed-panel);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 35%);
|
||||
}
|
||||
.notification-heading,
|
||||
.notification-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.notification-heading {
|
||||
padding: 9px 10px;
|
||||
border-bottom: 1px solid var(--ui-divider);
|
||||
color: var(--ed-fg);
|
||||
font: 600 11px var(--font-data);
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
.notification-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--ui-divider);
|
||||
opacity: 0.65;
|
||||
}
|
||||
.notification-item.unseen {
|
||||
opacity: 1;
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
.notification-title {
|
||||
color: var(--ed-fg);
|
||||
font-weight: 600;
|
||||
}
|
||||
.notification-message {
|
||||
margin: 6px 0 9px;
|
||||
color: var(--ed-dim);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.notification-empty {
|
||||
padding: 18px 10px;
|
||||
color: var(--ed-dim);
|
||||
font: 11px var(--font-data);
|
||||
}
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 214px 1fr;
|
||||
|
||||
Reference in New Issue
Block a user