useNotifications
The list behind the navbar bell. A Pinia store, persisted, so the notifications and what has been read survive a reload.
vue
<script setup lang="ts">
import { useNotifications } from '@shared/composables/useNotifications'
import { Badge } from '@shared/components/ui/badge'
import { Button } from '@shared/components/ui/button'
const notifications = useNotifications()
function paymentArrived(): void {
notifications.add({
type: 'success',
title: 'Payment received',
description: 'Invoice INV-2043 has been paid in full.',
link: '/finance/invoices',
})
}
function capacityWarning(): void {
notifications.add({
type: 'warning',
title: 'Warehouse near capacity',
description: 'Felixstowe Hub is at 94% utilization.',
link: '/logistics/warehouses',
})
}
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2">
<Button size="sm" @click="paymentArrived()">
Add a success
</Button>
<Button size="sm" variant="outline" @click="capacityWarning()">
Add a warning
</Button>
<Button size="sm" variant="outline" :disabled="!notifications.unreadCount" @click="notifications.markAllRead()">
Mark all read
</Button>
<Badge>{{ notifications.unreadCount }} unread</Badge>
</div>
<ul v-if="notifications.items.length" class="flex flex-col gap-2">
<li
v-for="item in notifications.items.slice(0, 6)"
:key="item.id"
class="flex items-center justify-between gap-3 rounded-lg border border-border p-3"
>
<div>
<p class="sa-text-sm font-medium" :class="{ 'text-muted-foreground': item.read }">
{{ item.title }}
</p>
<p class="sa-text-xs text-muted-foreground">
{{ item.description }}
</p>
</div>
<div class="flex shrink-0 gap-1">
<Button v-if="!item.read" size="sm" variant="ghost" @click="notifications.markRead(item.id)">
Read
</Button>
<Button size="sm" variant="ghost" @click="notifications.remove(item.id)">
Remove
</Button>
</div>
</li>
</ul>
<p v-else class="sa-text-sm text-muted-foreground">
Nothing here. Add one above.
</p>
</div>
</template>Import
ts
import { useNotifications } from '@shared/composables/useNotifications'Signature
It is a Pinia setup store, not a plain composable. Its id is notifications, and it persists itself to localStorage under the key sa-notifications.
Calling it returns the store instance, with the refs already unwrapped, so notifications.items is a Notification[] and notifications.unreadCount a number:
ts
const notifications = useNotifications()Keep the instance rather than destructuring the state, as the template's own components do. storeToRefs gives you standalone refs when you want them; the actions are safe to destructure on their own.
Members
| Member | Type on the instance | Description |
|---|---|---|
items | Notification[] | Newest first. |
unreadCount | number | What the bell's badge shows. |
add | (n) => void | Unshifts one. The id, timestamp and read flag are filled in. |
markRead | (id) => void | Marks one read. |
markAllRead | () => void | Marks every one read. |
remove | (id) => void | Drops one. |
seedDemo | () => void | Fills the list with five fixed examples, only while it is empty. |
$reset | () => void | Empties it. |
Notification
| Key | Type | Description |
|---|---|---|
id | string | A UUID, assigned by add. |
type | 'info' | 'success' | 'warning' | 'error' | Drives the icon and its color. |
title | string | A translation key or a literal. |
description | string | The same. |
params | Record<string, string> | Interpolation values for a keyed title or description. |
link | string | Where clicking it goes. |
timestamp | Date | Set by add. |
read | boolean | Starts false. |
Example
vue
<script setup lang="ts">
import { useNotifications } from '@shared/composables/useNotifications'
const notifications = useNotifications()
function paymentArrived(invoice: string) {
notifications.add({
type: 'success',
title: 'Payment received',
description: `Invoice ${invoice} has been paid in full.`,
link: '/finance/invoices',
})
}
</script>
<template>
<div class="space-y-3">
<button type="button" @click="paymentArrived('INV-2043')">
Simulate a payment
</button>
<p>{{ notifications.unreadCount }} unread</p>
<ul>
<li v-for="item in notifications.items" :key="item.id">
<button type="button" @click="notifications.markRead(item.id)">
{{ item.title }}
</button>
</li>
</ul>
</div>
</template>Notes
titleanddescriptiongo throughuiText. A translation key translates and follows a language switch; any other string, such as one your server sent, renders as it is. That is what lets the same list hold both.timestampis aDatein memory and an ISO string once persisted. Consumers coerce withnew Date(...)before formatting, so a rehydrated string is handled either way. Do the same if you read it yourself.seedDemoonly fills an empty list, and its ids are fixed. The store is persisted, so an unconditional seed would stack a fresh copy on every reload, and random ids could not be deduplicated. Its timestamps are offsets from the first seeding, so the relative labels still read sensibly whenever the demo is opened.- Persisted under
sa-notifications. - The whole feature sits behind
config.features.notifications.
Related
- Notification center — the bell and its panel.
useRealtimeNotifications— pushing into this from a server.- Toasts — the transient kind, which this is not.