@codingfactory/notify-kit-client 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +500 -0
- package/dist/index.d.ts +170 -199
- package/dist/index.js +1127 -185
- package/dist/index.js.map +1 -1
- package/dist/nuxt/index.d.ts +2 -2
- package/dist/nuxt/index.js +1 -1
- package/dist/nuxt/index.js.map +1 -1
- package/dist/{useNotifyKitModals-BZ_NiTKw.d.ts → useNotifyKitModals-CWHM9rYY.d.ts} +205 -59
- package/package.json +1 -6
package/README.md
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# @coding-factory/notify-kit-client
|
|
2
|
+
|
|
3
|
+
Headless Vue 3/Nuxt client for [Notify Kit](../notify-kit/) notifications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **TypeScript-first** - Strict types matching the backend payload contract
|
|
8
|
+
- **HTTP Client** - Full API coverage with type-safe methods
|
|
9
|
+
- **Vue 3 Composables** - Headless, reactive state management
|
|
10
|
+
- **Nuxt Module** - SSR-safe plugin with auto-imports
|
|
11
|
+
- **Real-time Integration** - Laravel Echo subscription handling
|
|
12
|
+
- **Modal Queue Management** - Built-in critical notification modals
|
|
13
|
+
- **Toast Policy Helpers** - Decide what toasts and plays sounds
|
|
14
|
+
|
|
15
|
+
## Ownership
|
|
16
|
+
|
|
17
|
+
`@coding-factory/notify-kit-client` is the package-owned home for generic frontend notification behavior.
|
|
18
|
+
|
|
19
|
+
- Put typed notification API calls, unread-count refresh logic, inbox pagination, realtime merge rules, modal queue helpers, and preference-settings access here.
|
|
20
|
+
- Keep consumer apps thin: they should mostly provide rendering, routing, branded presentation, and app-specific side effects.
|
|
21
|
+
- Do not add a second app-local notification store or duplicate realtime subscription pipeline when the behavior can live in this package.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @coding-factory/notify-kit-client
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For Nuxt applications, no additional setup is needed for the module.
|
|
30
|
+
|
|
31
|
+
## Vue 3 Setup
|
|
32
|
+
|
|
33
|
+
### 1. Create and Provide the Client
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// main.ts
|
|
37
|
+
import { createApp, provide } from 'vue';
|
|
38
|
+
import {
|
|
39
|
+
createNotifyKitClient,
|
|
40
|
+
NOTIFY_KIT_CLIENT_KEY,
|
|
41
|
+
} from '@coding-factory/notify-kit-client';
|
|
42
|
+
import App from './App.vue';
|
|
43
|
+
|
|
44
|
+
const client = createNotifyKitClient({
|
|
45
|
+
baseUrl: '/api/notify-kit',
|
|
46
|
+
// Optional: provide auth headers
|
|
47
|
+
getAuthHeaders: () => ({
|
|
48
|
+
Authorization: `Bearer ${getToken()}`,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const app = createApp(App);
|
|
53
|
+
app.provide(NOTIFY_KIT_CLIENT_KEY, client);
|
|
54
|
+
app.mount('#app');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Use Composables in Components
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<script setup lang="ts">
|
|
61
|
+
import {
|
|
62
|
+
useNotifyKitClient,
|
|
63
|
+
useNotifyKitStore,
|
|
64
|
+
useNotifyKitModals,
|
|
65
|
+
} from '@coding-factory/notify-kit-client';
|
|
66
|
+
|
|
67
|
+
// Access the client for API calls
|
|
68
|
+
const { client } = useNotifyKitClient();
|
|
69
|
+
|
|
70
|
+
// Access reactive store state
|
|
71
|
+
const store = useNotifyKitStore();
|
|
72
|
+
const unreadCount = computed(() => store.state.unreadCount);
|
|
73
|
+
|
|
74
|
+
// Access modal queue
|
|
75
|
+
const { currentModal, hasModals, ack, snooze } = useNotifyKitModals();
|
|
76
|
+
</script>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Set Up Real-time with Echo
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// composables/useNotifications.ts
|
|
83
|
+
import Echo from 'laravel-echo';
|
|
84
|
+
import Pusher from 'pusher-js';
|
|
85
|
+
import {
|
|
86
|
+
useNotifyKitClient,
|
|
87
|
+
useNotifyKitRealtime,
|
|
88
|
+
useNotifyKitStore,
|
|
89
|
+
defaultToastPolicy,
|
|
90
|
+
} from '@coding-factory/notify-kit-client';
|
|
91
|
+
|
|
92
|
+
// Configure Echo (one-time setup)
|
|
93
|
+
window.Pusher = Pusher;
|
|
94
|
+
const echo = new Echo({
|
|
95
|
+
broadcaster: 'reverb',
|
|
96
|
+
key: import.meta.env.VITE_REVERB_APP_KEY,
|
|
97
|
+
wsHost: import.meta.env.VITE_REVERB_HOST,
|
|
98
|
+
wsPort: import.meta.env.VITE_REVERB_PORT,
|
|
99
|
+
forceTLS: false,
|
|
100
|
+
enabledTransports: ['ws'],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export function useNotifications() {
|
|
104
|
+
const { client } = useNotifyKitClient();
|
|
105
|
+
const store = useNotifyKitStore();
|
|
106
|
+
|
|
107
|
+
const { connect, disconnect, isConnected } = useNotifyKitRealtime({
|
|
108
|
+
echo: () => echo,
|
|
109
|
+
autoConnect: true,
|
|
110
|
+
onNotification: (notification) => {
|
|
111
|
+
// Handle new notification
|
|
112
|
+
if (defaultToastPolicy.shouldToast(notification)) {
|
|
113
|
+
showToast(notification);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
connect,
|
|
120
|
+
disconnect,
|
|
121
|
+
isConnected,
|
|
122
|
+
unreadCount: computed(() => store.state.unreadCount),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Nuxt 3 Setup
|
|
128
|
+
|
|
129
|
+
### 1. Add the Module
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// nuxt.config.ts
|
|
133
|
+
export default defineNuxtConfig({
|
|
134
|
+
modules: ['@coding-factory/notify-kit-client/nuxt'],
|
|
135
|
+
|
|
136
|
+
notifyKit: {
|
|
137
|
+
baseUrl: '/api/notify-kit',
|
|
138
|
+
autoConnect: true,
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Proxy API to Laravel backend
|
|
142
|
+
routeRules: {
|
|
143
|
+
'/api/**': { proxy: 'http://localhost:8000/api/**' },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 2. Use the Auto-imported Composable
|
|
149
|
+
|
|
150
|
+
```vue
|
|
151
|
+
<script setup lang="ts">
|
|
152
|
+
// useNotifyKitNuxt is auto-imported
|
|
153
|
+
const { client, state, fetchUnreadCount } = useNotifyKitNuxt();
|
|
154
|
+
|
|
155
|
+
onMounted(async () => {
|
|
156
|
+
await fetchUnreadCount();
|
|
157
|
+
});
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<div>
|
|
162
|
+
<span>{{ state.unreadCount }} unread</span>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### SSR Considerations
|
|
168
|
+
|
|
169
|
+
The Nuxt plugin handles SSR safety automatically. For components that use browser APIs (like Echo), wrap them in `<ClientOnly>`:
|
|
170
|
+
|
|
171
|
+
```vue
|
|
172
|
+
<template>
|
|
173
|
+
<ClientOnly>
|
|
174
|
+
<ModalManager />
|
|
175
|
+
<ToastContainer />
|
|
176
|
+
</ClientOnly>
|
|
177
|
+
</template>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Composables Reference
|
|
181
|
+
|
|
182
|
+
### useNotifyKitClient
|
|
183
|
+
|
|
184
|
+
Access the HTTP client for API calls.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const { client } = useNotifyKitClient();
|
|
188
|
+
|
|
189
|
+
// API methods
|
|
190
|
+
await client.listNotifications({ unread: true, per_page: 10 });
|
|
191
|
+
await client.getUnreadCount();
|
|
192
|
+
await client.markRead(notificationId);
|
|
193
|
+
await client.markAllRead();
|
|
194
|
+
await client.deleteNotification(notificationId);
|
|
195
|
+
await client.listModals();
|
|
196
|
+
await client.ackModal(modalId);
|
|
197
|
+
await client.snoozeModal(modalId, 15); // minutes
|
|
198
|
+
await client.getPreferences();
|
|
199
|
+
await client.updatePreferences({ categories: { ... } });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### useNotifyKitStore
|
|
203
|
+
|
|
204
|
+
Centralized reactive state for notifications.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const store = useNotifyKitStore();
|
|
208
|
+
|
|
209
|
+
// Reactive state
|
|
210
|
+
store.state.notifications; // NotifyKitNotification[]
|
|
211
|
+
store.state.unreadCount; // number
|
|
212
|
+
store.state.modalQueue; // NotifyKitNotification[]
|
|
213
|
+
store.state.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'
|
|
214
|
+
store.state.broadcastChannel; // string | null
|
|
215
|
+
|
|
216
|
+
// Computed helpers
|
|
217
|
+
store.hasUnread; // ComputedRef<boolean>
|
|
218
|
+
store.currentModal; // ComputedRef<NotifyKitNotification | null>
|
|
219
|
+
store.hasModals; // ComputedRef<boolean>
|
|
220
|
+
|
|
221
|
+
// Actions
|
|
222
|
+
store.addNotification(notification);
|
|
223
|
+
store.markNotificationRead(id);
|
|
224
|
+
store.removeNotification(id);
|
|
225
|
+
store.incrementUnreadCount();
|
|
226
|
+
store.decrementUnreadCount();
|
|
227
|
+
store.enqueueModal(notification);
|
|
228
|
+
store.dequeueModal(id);
|
|
229
|
+
store.setConnectionState(state);
|
|
230
|
+
store.setBroadcastChannel(channel);
|
|
231
|
+
store.reset();
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### useNotifyKitRealtime
|
|
235
|
+
|
|
236
|
+
Real-time subscription management via Laravel Echo.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const {
|
|
240
|
+
isConnected,
|
|
241
|
+
connectionState,
|
|
242
|
+
connect,
|
|
243
|
+
disconnect,
|
|
244
|
+
reconnect,
|
|
245
|
+
} = useNotifyKitRealtime({
|
|
246
|
+
echo: () => echoInstance,
|
|
247
|
+
autoConnect: true,
|
|
248
|
+
onNotification: (notification) => {
|
|
249
|
+
// Called when notification received
|
|
250
|
+
},
|
|
251
|
+
onConnectionChange: (state) => {
|
|
252
|
+
// Called when connection state changes
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### useNotifyKitModals
|
|
258
|
+
|
|
259
|
+
Modal queue management for critical notifications.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
const {
|
|
263
|
+
currentModal, // Current modal to display (first in queue)
|
|
264
|
+
modalQueue, // All queued modals
|
|
265
|
+
hasModals, // Are there modals to show?
|
|
266
|
+
isLoading, // Loading outstanding modals?
|
|
267
|
+
loadOutstandingModals, // Fetch from API on login/refresh
|
|
268
|
+
ack, // Acknowledge a modal
|
|
269
|
+
snooze, // Snooze a modal
|
|
270
|
+
dismissCurrent, // Ack current modal
|
|
271
|
+
snoozeCurrent, // Snooze current modal
|
|
272
|
+
getSnoozeOptions, // Get allowed snooze durations
|
|
273
|
+
} = useNotifyKitModals();
|
|
274
|
+
|
|
275
|
+
// Acknowledge
|
|
276
|
+
await ack(notificationId);
|
|
277
|
+
|
|
278
|
+
// Snooze for 60 minutes
|
|
279
|
+
await snooze(notificationId, 60);
|
|
280
|
+
|
|
281
|
+
// Get snooze options from modal spec
|
|
282
|
+
const options = getSnoozeOptions(notification);
|
|
283
|
+
// [15, 60, 240, 1440]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Toast Policy Helpers
|
|
287
|
+
|
|
288
|
+
Decide which notifications should display as toasts and play sounds.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import {
|
|
292
|
+
defaultToastPolicy,
|
|
293
|
+
createToastPolicy,
|
|
294
|
+
} from '@coding-factory/notify-kit-client';
|
|
295
|
+
|
|
296
|
+
// Default policy
|
|
297
|
+
// - Critical modals (requires_ack): no toast (modal is the UX)
|
|
298
|
+
// - Social category: no toast (too frequent)
|
|
299
|
+
// - Security category: always toast + play sound
|
|
300
|
+
// - Danger level: play sound
|
|
301
|
+
// - Everything else: toast, no sound
|
|
302
|
+
|
|
303
|
+
if (defaultToastPolicy.shouldToast(notification)) {
|
|
304
|
+
showToast(notification);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (defaultToastPolicy.shouldPlaySound(notification)) {
|
|
308
|
+
playSound();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Custom policy
|
|
312
|
+
const customPolicy = createToastPolicy({
|
|
313
|
+
// Enable toasts for social
|
|
314
|
+
toastCategories: {
|
|
315
|
+
social: true,
|
|
316
|
+
},
|
|
317
|
+
// Play sound for warnings too
|
|
318
|
+
soundLevels: ['warning', 'danger'],
|
|
319
|
+
// Play sound for orders
|
|
320
|
+
soundCategories: ['security', 'orders'],
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## TypeScript Types
|
|
325
|
+
|
|
326
|
+
All types are exported for use in your application.
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import type {
|
|
330
|
+
// Core notification types
|
|
331
|
+
NotifyKitNotification,
|
|
332
|
+
NotificationCategory,
|
|
333
|
+
NotificationLevel,
|
|
334
|
+
NotifyKitModalSpec,
|
|
335
|
+
NotifyKitModalState,
|
|
336
|
+
|
|
337
|
+
// API response types
|
|
338
|
+
NotifyKitMeResponse,
|
|
339
|
+
NotifyKitListResponse,
|
|
340
|
+
NotifyKitGroupedEntry,
|
|
341
|
+
NotifyKitUnreadCountResponse,
|
|
342
|
+
NotifyKitPreferences,
|
|
343
|
+
PaginationMeta,
|
|
344
|
+
|
|
345
|
+
// Client configuration
|
|
346
|
+
NotifyKitClientConfig,
|
|
347
|
+
} from '@coding-factory/notify-kit-client';
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Type Guards
|
|
351
|
+
|
|
352
|
+
Validate data at runtime:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import {
|
|
356
|
+
isNotifyKitNotification,
|
|
357
|
+
isModalEnabled,
|
|
358
|
+
isValidCategory,
|
|
359
|
+
isValidLevel,
|
|
360
|
+
isPaginationMeta,
|
|
361
|
+
isMeResponse,
|
|
362
|
+
isUnreadCountResponse,
|
|
363
|
+
isModalSpec,
|
|
364
|
+
isChannelPreferences,
|
|
365
|
+
} from '@coding-factory/notify-kit-client';
|
|
366
|
+
|
|
367
|
+
// Validate incoming notification from Echo
|
|
368
|
+
if (isNotifyKitNotification(data)) {
|
|
369
|
+
store.addNotification(data);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check if notification has active modal
|
|
373
|
+
if (isModalEnabled(notification)) {
|
|
374
|
+
store.enqueueModal(notification);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Recommended UX Patterns
|
|
379
|
+
|
|
380
|
+
### Notification Bell
|
|
381
|
+
|
|
382
|
+
```vue
|
|
383
|
+
<script setup lang="ts">
|
|
384
|
+
const { client, state } = useNotifyKitNuxt();
|
|
385
|
+
|
|
386
|
+
async function loadNotifications() {
|
|
387
|
+
const response = await client.listNotifications({ per_page: 10 });
|
|
388
|
+
state.notifications = response.data;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function markAsRead(id: string) {
|
|
392
|
+
await client.markRead(id);
|
|
393
|
+
// Optimistically update UI
|
|
394
|
+
const notification = state.notifications.find(n => n.id === id);
|
|
395
|
+
if (notification) notification.read_at = new Date().toISOString();
|
|
396
|
+
state.unreadCount--;
|
|
397
|
+
}
|
|
398
|
+
</script>
|
|
399
|
+
|
|
400
|
+
<template>
|
|
401
|
+
<div class="bell">
|
|
402
|
+
<button @click="toggle">
|
|
403
|
+
🔔
|
|
404
|
+
<span v-if="state.unreadCount > 0" class="badge">
|
|
405
|
+
{{ state.unreadCount }}
|
|
406
|
+
</span>
|
|
407
|
+
</button>
|
|
408
|
+
<!-- Dropdown with notification list -->
|
|
409
|
+
</div>
|
|
410
|
+
</template>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Modal Manager
|
|
414
|
+
|
|
415
|
+
```vue
|
|
416
|
+
<script setup lang="ts">
|
|
417
|
+
const { currentModal, hasModals, ack, snooze, getSnoozeOptions } = useNotifyKitModals();
|
|
418
|
+
|
|
419
|
+
async function handleAck() {
|
|
420
|
+
if (currentModal.value) {
|
|
421
|
+
await ack(currentModal.value.id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function handleSnooze(minutes: number) {
|
|
426
|
+
if (currentModal.value) {
|
|
427
|
+
await snooze(currentModal.value.id, minutes);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
</script>
|
|
431
|
+
|
|
432
|
+
<template>
|
|
433
|
+
<Teleport to="body">
|
|
434
|
+
<div v-if="hasModals && currentModal" class="modal-overlay">
|
|
435
|
+
<div class="modal">
|
|
436
|
+
<h2>{{ currentModal.title }}</h2>
|
|
437
|
+
<p>{{ currentModal.body }}</p>
|
|
438
|
+
|
|
439
|
+
<div class="actions">
|
|
440
|
+
<select @change="handleSnooze(Number($event.target.value))">
|
|
441
|
+
<option disabled selected>Remind me later</option>
|
|
442
|
+
<option v-for="min in getSnoozeOptions(currentModal)" :value="min">
|
|
443
|
+
{{ formatMinutes(min) }}
|
|
444
|
+
</option>
|
|
445
|
+
</select>
|
|
446
|
+
|
|
447
|
+
<button @click="handleAck">Acknowledge</button>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</Teleport>
|
|
452
|
+
</template>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Examples
|
|
456
|
+
|
|
457
|
+
See the [examples directory](./examples/) for complete working examples:
|
|
458
|
+
|
|
459
|
+
- **Vue 3 App** - `examples/vue-app/`
|
|
460
|
+
- **Nuxt 3 App** - `examples/nuxt-app/`
|
|
461
|
+
|
|
462
|
+
## API Reference
|
|
463
|
+
|
|
464
|
+
### Client Configuration
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
interface NotifyKitClientConfig {
|
|
468
|
+
/** Base URL for the API (e.g., '/api/notify-kit') */
|
|
469
|
+
baseUrl: string;
|
|
470
|
+
|
|
471
|
+
/** Optional function to provide auth headers */
|
|
472
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
473
|
+
|
|
474
|
+
/** Optional custom fetch function */
|
|
475
|
+
fetch?: typeof fetch;
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Module Options (Nuxt)
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
interface NotifyKitModuleOptions {
|
|
483
|
+
/** Base URL for the API (default: '/api/notify-kit') */
|
|
484
|
+
baseUrl?: string;
|
|
485
|
+
|
|
486
|
+
/** Auto-connect to real-time on client (default: true) */
|
|
487
|
+
autoConnect?: boolean;
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Requirements
|
|
492
|
+
|
|
493
|
+
- Vue 3.4+
|
|
494
|
+
- TypeScript 5.0+
|
|
495
|
+
- For real-time: Laravel Echo + Pusher.js (or Reverb)
|
|
496
|
+
- Backend: [notify-kit](../notify-kit/) Laravel package
|
|
497
|
+
|
|
498
|
+
## License
|
|
499
|
+
|
|
500
|
+
MIT
|