@djangocfg/nextjs 2.1.36 → 2.1.38

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.
Files changed (43) hide show
  1. package/README.md +146 -1
  2. package/dist/config/index.d.mts +7 -428
  3. package/dist/config/index.mjs +80 -396
  4. package/dist/config/index.mjs.map +1 -1
  5. package/dist/index.d.mts +2 -1
  6. package/dist/index.mjs +80 -396
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/plugin-DuRJ_Jq6.d.mts +100 -0
  9. package/dist/pwa/cli.d.mts +1 -0
  10. package/dist/pwa/cli.mjs +140 -0
  11. package/dist/pwa/cli.mjs.map +1 -0
  12. package/dist/pwa/index.d.mts +274 -0
  13. package/dist/pwa/index.mjs +327 -0
  14. package/dist/pwa/index.mjs.map +1 -0
  15. package/dist/pwa/server/index.d.mts +86 -0
  16. package/dist/pwa/server/index.mjs +175 -0
  17. package/dist/pwa/server/index.mjs.map +1 -0
  18. package/dist/pwa/server/routes.d.mts +2 -0
  19. package/dist/pwa/server/routes.mjs +149 -0
  20. package/dist/pwa/server/routes.mjs.map +1 -0
  21. package/dist/pwa/worker/index.d.mts +56 -0
  22. package/dist/pwa/worker/index.mjs +97 -0
  23. package/dist/pwa/worker/index.mjs.map +1 -0
  24. package/dist/routes-DXA29sS_.d.mts +68 -0
  25. package/package.json +39 -8
  26. package/src/config/createNextConfig.ts +9 -13
  27. package/src/config/index.ts +2 -20
  28. package/src/config/plugins/devStartup.ts +35 -36
  29. package/src/config/plugins/index.ts +1 -1
  30. package/src/config/utils/index.ts +0 -1
  31. package/src/index.ts +4 -0
  32. package/src/pwa/cli.ts +171 -0
  33. package/src/pwa/index.ts +9 -0
  34. package/src/pwa/manifest.ts +355 -0
  35. package/src/pwa/notifications.ts +192 -0
  36. package/src/pwa/plugin.ts +194 -0
  37. package/src/pwa/server/index.ts +23 -0
  38. package/src/pwa/server/push.ts +166 -0
  39. package/src/pwa/server/routes.ts +137 -0
  40. package/src/pwa/worker/index.ts +174 -0
  41. package/src/pwa/worker/package.json +3 -0
  42. package/src/config/plugins/pwa.ts +0 -616
  43. package/src/config/utils/manifest.ts +0 -214
@@ -0,0 +1,192 @@
1
+ /**
2
+ * PWA Push Notifications Client Utilities
3
+ *
4
+ * Functions for requesting permission, subscribing to push notifications,
5
+ * and sending test notifications
6
+ */
7
+
8
+ /**
9
+ * Check if push notifications are supported
10
+ */
11
+ export function isPushNotificationSupported(): boolean {
12
+ return (
13
+ 'serviceWorker' in navigator &&
14
+ 'PushManager' in window &&
15
+ 'Notification' in window
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Get current notification permission status
21
+ */
22
+ export function getNotificationPermission(): NotificationPermission {
23
+ if (!('Notification' in window)) {
24
+ return 'denied';
25
+ }
26
+ return Notification.permission;
27
+ }
28
+
29
+ /**
30
+ * Request notification permission from user
31
+ */
32
+ export async function requestNotificationPermission(): Promise<NotificationPermission> {
33
+ if (!('Notification' in window)) {
34
+ throw new Error('Notifications are not supported');
35
+ }
36
+
37
+ const permission = await Notification.requestPermission();
38
+ return permission;
39
+ }
40
+
41
+ /**
42
+ * Convert base64 VAPID key to Uint8Array
43
+ */
44
+ function urlBase64ToUint8Array(base64String: string): Uint8Array {
45
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
46
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
47
+
48
+ const rawData = window.atob(base64);
49
+ const outputArray = new Uint8Array(rawData.length);
50
+
51
+ for (let i = 0; i < rawData.length; ++i) {
52
+ outputArray[i] = rawData.charCodeAt(i);
53
+ }
54
+ return outputArray;
55
+ }
56
+
57
+ export interface PushSubscriptionOptions {
58
+ /**
59
+ * VAPID public key (base64 encoded)
60
+ * Generate with: npx web-push generate-vapid-keys
61
+ */
62
+ vapidPublicKey: string;
63
+
64
+ /**
65
+ * User visible only (required for Chrome)
66
+ * @default true
67
+ */
68
+ userVisibleOnly?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Subscribe to push notifications
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const subscription = await subscribeToPushNotifications({
77
+ * vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
78
+ * });
79
+ *
80
+ * // Send subscription to your backend
81
+ * await fetch('/api/push/subscribe', {
82
+ * method: 'POST',
83
+ * headers: { 'Content-Type': 'application/json' },
84
+ * body: JSON.stringify(subscription),
85
+ * });
86
+ * ```
87
+ */
88
+ export async function subscribeToPushNotifications(
89
+ options: PushSubscriptionOptions
90
+ ): Promise<PushSubscription> {
91
+ if (!isPushNotificationSupported()) {
92
+ throw new Error('Push notifications are not supported');
93
+ }
94
+
95
+ // Request permission first
96
+ const permission = await requestNotificationPermission();
97
+ if (permission !== 'granted') {
98
+ throw new Error(`Notification permission ${permission}`);
99
+ }
100
+
101
+ // Get service worker registration
102
+ const registration = await navigator.serviceWorker.ready;
103
+
104
+ // Check if already subscribed
105
+ let subscription = await registration.pushManager.getSubscription();
106
+
107
+ if (subscription) {
108
+ return subscription;
109
+ }
110
+
111
+ // Subscribe to push notifications
112
+ const convertedVapidKey = urlBase64ToUint8Array(options.vapidPublicKey);
113
+
114
+ subscription = await registration.pushManager.subscribe({
115
+ userVisibleOnly: options.userVisibleOnly ?? true,
116
+ applicationServerKey: convertedVapidKey as BufferSource,
117
+ });
118
+
119
+ return subscription;
120
+ }
121
+
122
+ /**
123
+ * Unsubscribe from push notifications
124
+ */
125
+ export async function unsubscribeFromPushNotifications(): Promise<boolean> {
126
+ if (!isPushNotificationSupported()) {
127
+ return false;
128
+ }
129
+
130
+ const registration = await navigator.serviceWorker.ready;
131
+ const subscription = await registration.pushManager.getSubscription();
132
+
133
+ if (subscription) {
134
+ return await subscription.unsubscribe();
135
+ }
136
+
137
+ return false;
138
+ }
139
+
140
+ /**
141
+ * Get current push subscription
142
+ */
143
+ export async function getPushSubscription(): Promise<PushSubscription | null> {
144
+ if (!isPushNotificationSupported()) {
145
+ return null;
146
+ }
147
+
148
+ const registration = await navigator.serviceWorker.ready;
149
+ return await registration.pushManager.getSubscription();
150
+ }
151
+
152
+ /**
153
+ * Show a local notification (doesn't require push)
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * await showLocalNotification({
158
+ * title: 'Hello!',
159
+ * body: 'This is a test notification',
160
+ * icon: '/icon.png',
161
+ * data: { url: '/some-page' },
162
+ * });
163
+ * ```
164
+ */
165
+ export async function showLocalNotification(options: {
166
+ title: string;
167
+ body?: string;
168
+ icon?: string;
169
+ badge?: string;
170
+ tag?: string;
171
+ data?: any;
172
+ requireInteraction?: boolean;
173
+ }): Promise<void> {
174
+ if (!('Notification' in window)) {
175
+ throw new Error('Notifications are not supported');
176
+ }
177
+
178
+ const permission = await requestNotificationPermission();
179
+ if (permission !== 'granted') {
180
+ throw new Error(`Notification permission ${permission}`);
181
+ }
182
+
183
+ const registration = await navigator.serviceWorker.ready;
184
+ await registration.showNotification(options.title, {
185
+ body: options.body,
186
+ icon: options.icon,
187
+ badge: options.badge,
188
+ tag: options.tag,
189
+ data: options.data,
190
+ requireInteraction: options.requireInteraction,
191
+ });
192
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * PWA (Progressive Web App) Plugin
3
+ *
4
+ * Configures Serwist for service worker and offline support
5
+ * Modern PWA solution for Next.js 15+ with App Router
6
+ *
7
+ * @see https://serwist.pages.dev/
8
+ */
9
+
10
+ import type { NextConfig } from 'next';
11
+ import { consola } from 'consola';
12
+
13
+ export interface PWAPluginOptions {
14
+ /**
15
+ * Destination directory for service worker files
16
+ * @default 'public'
17
+ * @deprecated Use swDest instead
18
+ */
19
+ dest?: string;
20
+
21
+ /**
22
+ * Path to service worker source file (relative to project root)
23
+ * @default 'app/sw.ts'
24
+ */
25
+ swSrc?: string;
26
+
27
+ /**
28
+ * Destination for compiled service worker
29
+ * @default 'public/sw.js'
30
+ */
31
+ swDest?: string;
32
+
33
+ /**
34
+ * Disable PWA completely
35
+ * @default false in production, true in development
36
+ * @example disable: process.env.NODE_ENV === 'development'
37
+ */
38
+ disable?: boolean;
39
+
40
+ /**
41
+ * Cache on navigation - cache pages when navigating
42
+ * @default true
43
+ */
44
+ cacheOnNavigation?: boolean;
45
+
46
+ /**
47
+ * Reload app when device goes back online
48
+ * @default true
49
+ */
50
+ reloadOnOnline?: boolean;
51
+
52
+ /**
53
+ * Additional Serwist options
54
+ * @see https://serwist.pages.dev/docs/next/configuring
55
+ */
56
+ serwistOptions?: Record<string, any>;
57
+ }
58
+
59
+ /**
60
+ * Add PWA configuration to Next.js config using Serwist
61
+ *
62
+ * @example Basic usage
63
+ * ```ts
64
+ * import { createBaseNextConfig, withPWA } from '@djangocfg/nextjs/config';
65
+ *
66
+ * const nextConfig = createBaseNextConfig({...});
67
+ *
68
+ * export default withPWA(nextConfig, {
69
+ * swSrc: 'app/sw.ts',
70
+ * disable: process.env.NODE_ENV === 'development',
71
+ * });
72
+ * ```
73
+ *
74
+ * @example Integrated with createBaseNextConfig
75
+ * ```ts
76
+ * import { createBaseNextConfig } from '@djangocfg/nextjs/config';
77
+ *
78
+ * const config = createBaseNextConfig({
79
+ * pwa: {
80
+ * swSrc: 'app/sw.ts',
81
+ * disable: false,
82
+ * },
83
+ * });
84
+ *
85
+ * export default config;
86
+ * ```
87
+ */
88
+ export function withPWA(
89
+ nextConfig: NextConfig,
90
+ options: PWAPluginOptions = {}
91
+ ): NextConfig {
92
+ const isDev = process.env.NODE_ENV === 'development';
93
+
94
+ const defaultOptions: PWAPluginOptions = {
95
+ swSrc: 'app/sw.ts',
96
+ swDest: 'public/sw.js',
97
+ disable: options.disable !== undefined ? options.disable : isDev,
98
+ cacheOnNavigation: true,
99
+ reloadOnOnline: true,
100
+ ...options,
101
+ };
102
+
103
+ try {
104
+ const withSerwistInit = require('@serwist/next').default;
105
+
106
+ const withSerwist = withSerwistInit({
107
+ swSrc: defaultOptions.swSrc,
108
+ swDest: defaultOptions.swDest,
109
+ disable: defaultOptions.disable,
110
+ cacheOnNavigation: defaultOptions.cacheOnNavigation,
111
+ reloadOnOnline: defaultOptions.reloadOnOnline,
112
+ ...defaultOptions.serwistOptions,
113
+ });
114
+
115
+ return withSerwist(nextConfig);
116
+ } catch (error) {
117
+ consola.error('Failed to configure Serwist:', error);
118
+ return nextConfig;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get service worker template content
124
+ *
125
+ * Returns ready-to-use service worker code for app/sw.ts
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * import { getServiceWorkerTemplate } from '@djangocfg/nextjs/config';
130
+ *
131
+ * // Copy this to your app/sw.ts file
132
+ * console.log(getServiceWorkerTemplate());
133
+ * ```
134
+ */
135
+ export function getServiceWorkerTemplate(): string {
136
+ return `/**
137
+ * Service Worker (Serwist)
138
+ *
139
+ * Modern PWA service worker using Serwist
140
+ */
141
+
142
+ import { defaultCache } from '@serwist/next/worker';
143
+ import { Serwist } from 'serwist';
144
+
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ declare const self: any;
147
+
148
+ const serwist = new Serwist({
149
+ // Precache entries injected by Serwist build plugin
150
+ precacheEntries: self.__SW_MANIFEST,
151
+
152
+ // Skip waiting - activate new SW immediately
153
+ skipWaiting: true,
154
+
155
+ // Take control of all clients immediately
156
+ clientsClaim: true,
157
+
158
+ // Enable navigation preload for faster loads
159
+ navigationPreload: true,
160
+
161
+ // Use default Next.js runtime caching strategies
162
+ runtimeCaching: defaultCache,
163
+
164
+ // Fallback pages for offline
165
+ fallbacks: {
166
+ entries: [
167
+ {
168
+ url: '/_offline',
169
+ matcher({ request }) {
170
+ return request.destination === 'document';
171
+ },
172
+ },
173
+ ],
174
+ },
175
+ });
176
+
177
+ serwist.addEventListeners();
178
+ `;
179
+ }
180
+
181
+ // Backward compatibility exports (deprecated)
182
+ export const defaultRuntimeCaching = [];
183
+ export function createApiCacheRule() {
184
+ consola.warn('createApiCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');
185
+ return {};
186
+ }
187
+ export function createStaticAssetRule() {
188
+ consola.warn('createStaticAssetRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');
189
+ return {};
190
+ }
191
+ export function createCdnCacheRule() {
192
+ consola.warn('createCdnCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');
193
+ return {};
194
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Server-side PWA Utilities
3
+ *
4
+ * Push notifications, VAPID configuration, and route handlers
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Configure VAPID keys
9
+ * import { configureVapid } from '@djangocfg/nextjs/pwa/server';
10
+ * configureVapid();
11
+ *
12
+ * // Use ready-made route handlers
13
+ * // app/api/push/subscribe/route.ts
14
+ * export { POST, GET } from '@djangocfg/nextjs/pwa/server/routes';
15
+ *
16
+ * // Or use utilities directly
17
+ * import { sendPushNotification } from '@djangocfg/nextjs/pwa/server';
18
+ * await sendPushNotification(subscription, { title: 'Hello!' });
19
+ * ```
20
+ */
21
+
22
+ export * from './push';
23
+ export * as routes from './routes';
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Server-side Push Notification Utilities
3
+ *
4
+ * VAPID-based Web Push notifications using web-push library
5
+ */
6
+
7
+ import webpush, { PushSubscription } from 'web-push';
8
+ import { consola } from 'consola';
9
+
10
+ let vapidConfigured = false;
11
+
12
+ /**
13
+ * Check if VAPID keys are configured
14
+ */
15
+ export function isVapidConfigured(): boolean {
16
+ return vapidConfigured;
17
+ }
18
+
19
+ /**
20
+ * Get VAPID keys from environment
21
+ */
22
+ export function getVapidKeys() {
23
+ const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
24
+ const privateKey = process.env.VAPID_PRIVATE_KEY;
25
+ const mailto = process.env.VAPID_MAILTO || 'mailto:noreply@example.com';
26
+
27
+ return { publicKey, privateKey, mailto };
28
+ }
29
+
30
+ /**
31
+ * Configure VAPID keys for web-push
32
+ * Call this once at app startup
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // In your API route or middleware
37
+ * import { configureVapid } from '@djangocfg/nextjs/pwa/server';
38
+ *
39
+ * configureVapid(); // Uses env vars automatically
40
+ * ```
41
+ */
42
+ export function configureVapid(options?: {
43
+ publicKey?: string;
44
+ privateKey?: string;
45
+ mailto?: string;
46
+ }): boolean {
47
+ const { publicKey, privateKey, mailto } = options || getVapidKeys();
48
+
49
+ if (!publicKey || !privateKey) {
50
+ consola.warn(
51
+ '⚠️ VAPID keys not configured!\n' +
52
+ ' Generate keys: npx web-push generate-vapid-keys\n' +
53
+ ' Add to .env.local:\n' +
54
+ ' NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key\n' +
55
+ ' VAPID_PRIVATE_KEY=your_private_key\n' +
56
+ ' Push notifications will not work without VAPID keys.'
57
+ );
58
+ return false;
59
+ }
60
+
61
+ try {
62
+ webpush.setVapidDetails(mailto, publicKey, privateKey);
63
+ vapidConfigured = true;
64
+ consola.success('✅ VAPID keys configured for push notifications');
65
+ return true;
66
+ } catch (error: any) {
67
+ consola.error('Failed to configure VAPID keys:', error.message);
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Send push notification to a subscription
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * await sendPushNotification(subscription, {
78
+ * title: 'Hello!',
79
+ * body: 'Test notification',
80
+ * data: { url: '/page' },
81
+ * });
82
+ * ```
83
+ */
84
+ export async function sendPushNotification(
85
+ subscription: PushSubscription,
86
+ notification: {
87
+ title: string;
88
+ body?: string;
89
+ icon?: string;
90
+ badge?: string;
91
+ data?: any;
92
+ tag?: string;
93
+ requireInteraction?: boolean;
94
+ }
95
+ ): Promise<void> {
96
+ if (!vapidConfigured) {
97
+ const configured = configureVapid();
98
+ if (!configured) {
99
+ throw new Error('VAPID keys not configured. Cannot send push notification.');
100
+ }
101
+ }
102
+
103
+ const payload = JSON.stringify({
104
+ title: notification.title,
105
+ body: notification.body || '',
106
+ icon: notification.icon,
107
+ badge: notification.badge,
108
+ data: notification.data,
109
+ tag: notification.tag,
110
+ requireInteraction: notification.requireInteraction,
111
+ });
112
+
113
+ const result = await webpush.sendNotification(subscription, payload);
114
+ console.log('✅ Push Sent to FCM:', {
115
+ statusCode: result.statusCode,
116
+ headers: result.headers,
117
+ bodyLength: result.body?.length
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Send push notification to multiple subscriptions
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * const results = await sendPushToMultiple(subscriptions, {
127
+ * title: 'Broadcast message',
128
+ * body: 'Sent to all users',
129
+ * });
130
+ * console.log(`Sent: ${results.successful}, Failed: ${results.failed}`);
131
+ * ```
132
+ */
133
+ export async function sendPushToMultiple(
134
+ subscriptions: PushSubscription[],
135
+ notification: Parameters<typeof sendPushNotification>[1]
136
+ ): Promise<{
137
+ successful: number;
138
+ failed: number;
139
+ errors: Array<{ subscription: PushSubscription; error: Error }>;
140
+ }> {
141
+ const results = await Promise.allSettled(
142
+ subscriptions.map((sub) => sendPushNotification(sub, notification))
143
+ );
144
+
145
+ const successful = results.filter((r) => r.status === 'fulfilled').length;
146
+ const failed = results.filter((r) => r.status === 'rejected').length;
147
+ const errors = results
148
+ .map((r, i) => (r.status === 'rejected' ? { subscription: subscriptions[i], error: r.reason } : null))
149
+ .filter((e): e is NonNullable<typeof e> => e !== null);
150
+
151
+ return { successful, failed, errors };
152
+ }
153
+
154
+ /**
155
+ * Validate push subscription format
156
+ */
157
+ export function validateSubscription(subscription: any): subscription is PushSubscription {
158
+ return (
159
+ subscription &&
160
+ typeof subscription === 'object' &&
161
+ typeof subscription.endpoint === 'string' &&
162
+ subscription.keys &&
163
+ typeof subscription.keys.p256dh === 'string' &&
164
+ typeof subscription.keys.auth === 'string'
165
+ );
166
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Ready-to-use Push Notification Route Handlers
3
+ *
4
+ * Import these in your app/api/push/ routes
5
+ */
6
+
7
+ import { NextRequest, NextResponse } from 'next/server';
8
+ import { configureVapid, sendPushNotification, validateSubscription } from './push';
9
+
10
+ // In-memory storage для demo (в production используй БД)
11
+ const subscriptions = new Set<string>();
12
+
13
+ /**
14
+ * POST /api/push/subscribe
15
+ * Save push subscription
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // app/api/push/subscribe/route.ts
20
+ * export { POST } from '@djangocfg/nextjs/pwa/server/routes';
21
+ * ```
22
+ */
23
+ export async function handleSubscribe(request: NextRequest) {
24
+ try {
25
+ const subscription = await request.json();
26
+
27
+ if (!validateSubscription(subscription)) {
28
+ return NextResponse.json(
29
+ { error: 'Invalid subscription format' },
30
+ { status: 400 }
31
+ );
32
+ }
33
+
34
+ // Сохраняем subscription (в demo просто в памяти)
35
+ subscriptions.add(JSON.stringify(subscription));
36
+
37
+ console.log('✅ Push subscription saved:', {
38
+ endpoint: subscription.endpoint.substring(0, 50) + '...',
39
+ total: subscriptions.size,
40
+ });
41
+
42
+ return NextResponse.json({
43
+ success: true,
44
+ message: 'Subscription saved',
45
+ totalSubscriptions: subscriptions.size,
46
+ });
47
+ } catch (error: any) {
48
+ console.error('Subscription error:', error);
49
+
50
+ return NextResponse.json(
51
+ {
52
+ error: 'Failed to save subscription',
53
+ details: error.message,
54
+ },
55
+ { status: 500 }
56
+ );
57
+ }
58
+ }
59
+
60
+ /**
61
+ * GET /api/push/subscribe
62
+ * Get all subscriptions (for testing)
63
+ */
64
+ export async function handleGetSubscriptions() {
65
+ return NextResponse.json({
66
+ totalSubscriptions: subscriptions.size,
67
+ subscriptions: Array.from(subscriptions).map((sub) => {
68
+ const parsed = JSON.parse(sub);
69
+ return {
70
+ endpoint: parsed.endpoint.substring(0, 50) + '...',
71
+ };
72
+ }),
73
+ });
74
+ }
75
+
76
+ /**
77
+ * POST /api/push/send
78
+ * Send push notification
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * // app/api/push/send/route.ts
83
+ * export { POST as handleSend as POST } from '@djangocfg/nextjs/pwa/server/routes';
84
+ * ```
85
+ */
86
+ export async function handleSend(request: NextRequest) {
87
+ try {
88
+ const { subscription, notification } = await request.json();
89
+
90
+ if (!subscription) {
91
+ return NextResponse.json(
92
+ { error: 'Subscription is required' },
93
+ { status: 400 }
94
+ );
95
+ }
96
+
97
+ if (!validateSubscription(subscription)) {
98
+ return NextResponse.json(
99
+ { error: 'Invalid subscription format' },
100
+ { status: 400 }
101
+ );
102
+ }
103
+
104
+ // Configure VAPID if not already configured
105
+ configureVapid();
106
+
107
+ await sendPushNotification(subscription, {
108
+ title: notification?.title || 'Test Notification',
109
+ body: notification?.body || 'This is a test push notification',
110
+ icon: notification?.icon,
111
+ badge: notification?.badge,
112
+ data: notification?.data,
113
+ });
114
+
115
+ return NextResponse.json({
116
+ success: true,
117
+ message: 'Push notification sent',
118
+ });
119
+ } catch (error: any) {
120
+ console.error('Push notification error:', error);
121
+
122
+ return NextResponse.json(
123
+ {
124
+ error: 'Failed to send push notification',
125
+ details: error.message,
126
+ },
127
+ { status: 500 }
128
+ );
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Combined route handlers
134
+ * Use like: export { POST, GET } from '@djangocfg/nextjs/pwa/server/routes'
135
+ */
136
+ export const POST = handleSubscribe;
137
+ export const GET = handleGetSubscriptions;