Add user notification inbox and delivery controls
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import PageBreadcrumb from '$lib/components/PageBreadcrumb.svelte';
|
||||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||||
import ImageUploadInput from '$lib/components/ImageUploadInput.svelte';
|
||||
import { browserSupportsWebAuthn, startRegistration } from '@simplewebauthn/browser';
|
||||
@@ -24,6 +25,19 @@
|
||||
function initialAvatarPanel(): AvatarPanel {
|
||||
return form?.avatarError || form?.avatarReviewQueued ? 'upload' : 'presets';
|
||||
}
|
||||
const notificationIsIsac = (notification: any) => notification.category === 'isac' || String(notification.source).startsWith('ISAC');
|
||||
const notificationTone = (notification: any) => {
|
||||
const text = `${notification.title} ${notification.body}`.toLocaleLowerCase();
|
||||
if (/(approved|accepted|deployed|restored|successful)/.test(text)) return 'positive';
|
||||
if (/(removed|rejected|not approved|failed|could not|needs revision|violation)/.test(text)) return 'negative';
|
||||
return 'neutral';
|
||||
};
|
||||
const notificationSeverity = (notification: any) => {
|
||||
const text = `${notification.title} ${notification.body}`.toLocaleLowerCase();
|
||||
if (/(error|failed|could not)/.test(text)) return 'ERROR';
|
||||
if (/(warning|violation|needs revision)/.test(text)) return 'WARNING';
|
||||
return null;
|
||||
};
|
||||
let accountPanel = $state<AccountPanel>(initialAccountPanel());
|
||||
let accessPanel = $state<AccessPanel>(initialAccessPanel());
|
||||
let avatarPanel = $state<AvatarPanel>(initialAvatarPanel());
|
||||
@@ -65,8 +79,8 @@
|
||||
<svelte:head><title>{user.displayName} — SHD.build</title></svelte:head>
|
||||
<main class="page profile-page">
|
||||
<section class="profile-command">
|
||||
<div class="profile-ident"><img class="profile-avatar" src={user.avatarUrl} alt={`${user.displayName}'s avatar`} /><div><p class="eyebrow"><span></span> AGENT PROFILE</p><h1>{user.displayName}</h1><span class="role-badge">{user.role.toUpperCase()} ACCESS</span></div></div>
|
||||
<div class="profile-metrics"><div><strong>{data.account.builds.length}</strong><span>BUILDS</span></div><div><strong>{data.account.guides.length}</strong><span>GUIDES</span></div><div><strong>{data.account.favorites.length}</strong><span>FAVORITES</span></div><div><strong>{data.account.ratings.length}</strong><span>RATED</span></div></div>
|
||||
<div class="profile-ident"><img class="profile-avatar" src={user.avatarUrl} alt={`${user.displayName}'s avatar`} /><div><PageBreadcrumb items={[{ label: 'AGENT PROFILE' }]} /><h1>{user.displayName}</h1><span class="role-badge">{user.role.toUpperCase()} ACCESS</span></div></div>
|
||||
<div class="profile-metrics"><div><strong>{data.account.builds.length}</strong><span>BUILDS</span></div><div><strong>{data.account.guides.length}</strong><span>GUIDES</span></div><div><strong>{data.account.favorites.length}</strong><span>FAVORITES</span></div><div><strong>{data.account.ratings.length}</strong><span>RATED</span></div><a href="/account/notifications"><strong>{data.account.notificationUnread}</strong><span>INBOX</span></a></div>
|
||||
</section>
|
||||
{#if data.chooseDisplayName}<div class="account-alert"><b>IDENTITY SETUP</b><span>Choose the SHD.build display name other agents will see. Unicode characters are supported.</span></div>{/if}
|
||||
{#if data.discordLinked}<p class="success">Discord account linked successfully.</p>{/if}
|
||||
@@ -74,6 +88,14 @@
|
||||
{#if data.oauthError}<div class="account-alert error-alert" role="alert"><b>{data.oauthError.title}</b><span>{data.oauthError.message}</span></div>{/if}
|
||||
<div class="profile-dashboard">
|
||||
<section class="profile-feed">
|
||||
<div class="profile-module profile-notifications">
|
||||
<div class="module-head"><div><p class="eyebrow">SHD COMMUNICATION NETWORK</p><h2>NOTIFICATIONS</h2></div><a class="secondary" href="/account/notifications">OPEN INBOX{data.account.notificationUnread ? ` · ${data.account.notificationUnread} UNREAD` : ''} →</a></div>
|
||||
<div class="profile-notification-list">
|
||||
{#each data.account.notifications as notification}
|
||||
<a class:unread={!notification.read_at} class:mandatory={Boolean(notification.mandatory)} class:isac-message={notificationIsIsac(notification)} class:positive={notificationTone(notification) === 'positive'} class:negative={notificationTone(notification) === 'negative'} href="/account/notifications"><div><img src={notificationIsIsac(notification) ? '/favicon.svg' : '/avatars/default/comms-specialist.webp'} alt=""/><span><b>{notification.source}</b><small>{String(notification.category).toUpperCase()} · {new Date(String(notification.created_at)).toLocaleDateString()}</small></span></div><span><strong>{notification.title}</strong><small>{notification.body}</small></span><em>{notificationSeverity(notification) ?? 'OPEN'} ›</em></a>
|
||||
{:else}<div class="profile-empty compact"><span>◇</span><b>NO NOTIFICATIONS</b><p>ISAC and community messages will appear here.</p></div>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-module"><div class="module-head"><div><p class="eyebrow">LOADOUT ARCHIVE</p><h2>MY BUILDS</h2></div><a class="secondary" href="/builder">NEW BUILD +</a></div>
|
||||
{#if data.account.builds.length}<div class="profile-table"><div class="profile-row heading"><span>LOADOUT</span><span>VISIBILITY</span><span>UPDATED</span><span></span></div>{#each data.account.builds as build}<div class="profile-row"><div><strong>{build.title}</strong><small>/{build.slug}</small></div><span>{build.visibility}</span><span>{new Date(String(build.updated_at)).toLocaleDateString()}</span><button>OPEN →</button></div>{/each}</div>{:else}<div class="profile-empty"><span>◇</span><b>NO BUILDS RECORDED</b><p>Your saved and published loadouts will appear here.</p><a href="/builder">CREATE YOUR FIRST BUILD →</a></div>{/if}
|
||||
</div>
|
||||
@@ -89,7 +111,7 @@
|
||||
|
||||
{#if accountPanel === 'identity'}
|
||||
<div class="account-panel" id="identity-panel" role="tabpanel" aria-labelledby="identity-tab">
|
||||
<p class="eyebrow">AGENT PORTRAIT</p><h2>AVATAR</h2>
|
||||
<div id="avatar" class="profile-anchor" aria-hidden="true"></div><p class="eyebrow">AGENT PORTRAIT</p><h2>AVATAR</h2>
|
||||
<div class="account-subtabs" role="tablist" aria-label="Avatar source">
|
||||
<button class:active={avatarPanel === 'presets'} role="tab" aria-selected={avatarPanel === 'presets'} onclick={() => avatarPanel = 'presets'}>SHD PRESETS</button>
|
||||
<button class:active={avatarPanel === 'upload'} role="tab" aria-selected={avatarPanel === 'upload'} onclick={() => avatarPanel = 'upload'}>CUSTOM UPLOAD</button>
|
||||
@@ -190,3 +212,15 @@
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.profile-notification-list a > div { grid-template-columns: 30px minmax(0, 1fr); align-items: center; }
|
||||
.profile-notification-list a > div > img { width: 30px; height: 30px; object-fit: cover; border: 1px solid #3b4446; border-radius: 50%; background: #111; }
|
||||
.profile-notification-list a > div > span { min-width: 0; display: grid; gap: 3px; }
|
||||
.profile-notification-list a.isac-message b { color: #8ecce5; }
|
||||
.profile-notification-list a.positive { border-left: 2px solid var(--shd-success); background: #101713; }
|
||||
.profile-notification-list a.negative { border-left: 2px solid var(--shd-danger); background: #191111; }
|
||||
.profile-notification-list a.isac-message:not(.positive):not(.negative) { border-left: 2px solid #68a9c4; background: #10171a; }
|
||||
.profile-notification-list a.positive em { color: var(--shd-success); }
|
||||
.profile-notification-list a.negative em { color: var(--shd-danger); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user