mirror of
https://github.com/xtr-dev/payload-notifications.git
synced 2025-12-10 02:43:23 +00:00
Add initial plugin implementation and development setup
This commit is contained in:
13
dev/.env.example
Normal file
13
dev/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Database URL - will use in-memory DB for development if not provided
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-notifications-dev
|
||||
|
||||
# PayloadCMS Secret
|
||||
PAYLOAD_SECRET=your-super-secret-jwt-secret
|
||||
|
||||
# VAPID Keys for Web Push Notifications
|
||||
# Generate with: npx web-push generate-vapid-keys
|
||||
VAPID_PUBLIC_KEY=BMrF5MbHcaEo6w4lPjG9m3BvONvFPfz7jLJ9t0F9yJGzSI3ZUHQj9fNUP7w2D8h1kI4x3YzJ1a4f0nS5g6t2F9L
|
||||
VAPID_PRIVATE_KEY=your-vapid-private-key-here
|
||||
|
||||
# Development Settings
|
||||
NODE_ENV=development
|
||||
220
dev/README.md
Normal file
220
dev/README.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Payload Notifications Plugin - Development Environment
|
||||
|
||||
This is the development environment for testing and demonstrating the `@xtr-dev/payload-notifications` plugin.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Start the development server:**
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
3. **Open the demo:**
|
||||
- Homepage: [http://localhost:3000](http://localhost:3000)
|
||||
- Admin Panel: [http://localhost:3000/admin](http://localhost:3000/admin)
|
||||
- Push Demo: [http://localhost:3000/demo](http://localhost:3000/demo)
|
||||
|
||||
4. **Login to admin:**
|
||||
- Email: `dev@payloadcms.com`
|
||||
- Password: `test`
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
The dev environment showcases a complete implementation of the notifications plugin with:
|
||||
|
||||
### Collections
|
||||
- **Users** - Authentication with admin/customer roles
|
||||
- **Products** - Sample e-commerce products
|
||||
- **Orders** - Sample orders with different statuses
|
||||
- **Posts** - Blog posts for content notifications
|
||||
- **Notifications** - The plugin's notifications collection
|
||||
- **Push Subscriptions** - Web push subscription management
|
||||
|
||||
### Plugin Configuration
|
||||
```typescript
|
||||
notificationsPlugin({
|
||||
collections: {
|
||||
slug: 'notifications',
|
||||
labels: { singular: 'Notification', plural: 'Notifications' }
|
||||
},
|
||||
relationships: [
|
||||
{ name: 'order', relationTo: 'orders', label: 'Related Order' },
|
||||
{ name: 'product', relationTo: 'products', label: 'Related Product', hasMany: true },
|
||||
{ name: 'post', relationTo: 'posts', label: 'Related Post' }
|
||||
],
|
||||
access: {
|
||||
read: ({ req }) => Boolean(req.user),
|
||||
create: ({ req }) => Boolean(req.user),
|
||||
update: ({ req }) => Boolean(req.user),
|
||||
delete: ({ req }) => Boolean(req.user?.role === 'admin'),
|
||||
},
|
||||
webPush: {
|
||||
enabled: true,
|
||||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY,
|
||||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY,
|
||||
vapidSubject: 'mailto:test@example.com'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 📱 Web Push Notifications
|
||||
|
||||
### Setup VAPID Keys
|
||||
|
||||
1. **Generate VAPID keys:**
|
||||
```bash
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. **Create a `.env` file:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. **Add your VAPID keys to `.env`:**
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your-public-key
|
||||
VAPID_PRIVATE_KEY=your-private-key
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The plugin automatically creates these endpoints:
|
||||
|
||||
- `POST /api/push-notifications/subscribe` - Subscribe to push notifications
|
||||
- `POST /api/push-notifications/unsubscribe` - Unsubscribe from push notifications
|
||||
- `GET /api/push-notifications/vapid-public-key` - Get VAPID public key
|
||||
- `POST /api/push-notifications/send` - Send notification to user
|
||||
- `POST /api/push-notifications/test` - Send test notification (admin only)
|
||||
- `POST /api/push-notifications/track` - Track notification events
|
||||
|
||||
### Service Worker Setup
|
||||
|
||||
The service worker is already configured and located at `/public/sw.js`. For new projects, generate it with:
|
||||
|
||||
```bash
|
||||
npx @xtr-dev/payload-notifications generate-sw
|
||||
```
|
||||
|
||||
The service worker handles:
|
||||
|
||||
- **Push message processing** - Receives and displays push notifications
|
||||
- **Notification clicks** - Opens relevant admin panel or URLs
|
||||
- **Test notifications** - Supports demo functionality
|
||||
- **Analytics tracking** - Tracks notification interactions
|
||||
|
||||
### Testing Push Notifications
|
||||
|
||||
1. **Open the [demo page](http://localhost:3000/demo)**
|
||||
2. **Enable notifications:**
|
||||
- Click "Enable Notifications"
|
||||
- Allow browser permissions when prompted
|
||||
- The service worker will be registered automatically
|
||||
3. **Test the system:**
|
||||
- Click "Send Test Notification" to see instant notifications
|
||||
- Check browser dev tools console for service worker logs
|
||||
4. **Admin panel testing:**
|
||||
- Go to `/admin` and create notifications
|
||||
- Attach relationships to orders, products, or posts
|
||||
- Real push notifications require proper VAPID keys
|
||||
|
||||
### Service Worker Features
|
||||
|
||||
- ✅ **Automatic registration** when subscribing to notifications
|
||||
- ✅ **Test notification support** for immediate testing
|
||||
- ✅ **Rich notification display** with actions and custom icons
|
||||
- ✅ **Click handling** that opens relevant admin pages
|
||||
- ✅ **Analytics tracking** for notification interactions
|
||||
- ✅ **Fallback handling** for missing icons or data
|
||||
|
||||
## 📊 Sample Data
|
||||
|
||||
The development environment is automatically seeded with:
|
||||
|
||||
### Users
|
||||
- **Admin User**: dev@payloadcms.com (password: test)
|
||||
- **Customer User**: customer@example.com (password: test)
|
||||
|
||||
### Products
|
||||
- Wireless Headphones ($299.99)
|
||||
- Cotton T-Shirt ($24.99)
|
||||
- JavaScript Guide ($39.99)
|
||||
|
||||
### Orders
|
||||
- Order #ORD-001 (Shipped - Headphones + T-Shirt)
|
||||
- Order #ORD-002 (Pending - JavaScript Guide)
|
||||
|
||||
### Notifications
|
||||
- Welcome notification with blog post attachment
|
||||
- Order shipped notification with order and product attachments
|
||||
- Product recommendation notification (marked as read)
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### File Structure
|
||||
```
|
||||
dev/
|
||||
├── app/
|
||||
│ ├── (app)/
|
||||
│ │ ├── page.tsx # Homepage
|
||||
│ │ └── demo/
|
||||
│ │ └── page.tsx # Push notifications demo
|
||||
│ └── (payload)/
|
||||
│ ├── admin/ # Payload admin panel
|
||||
│ └── api/ # API routes
|
||||
├── helpers/
|
||||
│ ├── credentials.ts # Default user credentials
|
||||
│ └── testEmailAdapter.ts # Email testing
|
||||
├── payload.config.ts # Payload configuration
|
||||
├── seed.ts # Database seeding
|
||||
└── .env.example # Environment variables template
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
- `DATABASE_URI` - MongoDB connection string (optional, uses in-memory DB)
|
||||
- `PAYLOAD_SECRET` - JWT secret for authentication
|
||||
- `VAPID_PUBLIC_KEY` - VAPID public key for web push
|
||||
- `VAPID_PRIVATE_KEY` - VAPID private key for web push
|
||||
- `NODE_ENV` - Environment (development/production)
|
||||
|
||||
### Scripts
|
||||
- `pnpm dev` - Start development server
|
||||
- `pnpm build` - Build the application
|
||||
- `pnpm start` - Start production server
|
||||
- `pnpm lint` - Run ESLint
|
||||
- `pnpm test` - Run tests
|
||||
|
||||
## 🔍 Testing the Plugin
|
||||
|
||||
1. **Admin Panel Testing:**
|
||||
- Create notifications with different relationship attachments
|
||||
- Test read/unread functionality
|
||||
- View push subscriptions
|
||||
- Test user role permissions
|
||||
|
||||
2. **API Testing:**
|
||||
- Test push notification endpoints
|
||||
- Subscribe/unsubscribe from push notifications
|
||||
- Send test notifications
|
||||
|
||||
3. **Client Integration:**
|
||||
- Test the demo page functionality
|
||||
- Test push notification permissions
|
||||
- Test service worker integration
|
||||
|
||||
## 🚀 Production Deployment
|
||||
|
||||
1. Set up a real MongoDB database
|
||||
2. Configure proper VAPID keys
|
||||
3. Set up SSL certificates for push notifications
|
||||
4. Configure proper environment variables
|
||||
5. Deploy using your preferred platform
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For complete documentation, see the main [README.md](../README.md) file.
|
||||
493
dev/app/(app)/demo/page.tsx
Normal file
493
dev/app/(app)/demo/page.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// Enhanced demo implementation with real service worker registration
|
||||
class DemoClientPushManager {
|
||||
private vapidPublicKey: string
|
||||
private serviceWorkerPath: string
|
||||
private apiEndpoint: string
|
||||
|
||||
constructor(vapidPublicKey: string, options: { serviceWorkerPath?: string; apiEndpoint?: string } = {}) {
|
||||
this.vapidPublicKey = vapidPublicKey
|
||||
this.serviceWorkerPath = options.serviceWorkerPath || '/sw.js'
|
||||
this.apiEndpoint = options.apiEndpoint || '/api/push-notifications'
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
return (
|
||||
'serviceWorker' in navigator &&
|
||||
'PushManager' in window &&
|
||||
'Notification' in window
|
||||
)
|
||||
}
|
||||
|
||||
public getPermissionStatus(): NotificationPermission {
|
||||
if (typeof window === 'undefined' || typeof Notification === 'undefined') return 'default'
|
||||
return Notification.permission
|
||||
}
|
||||
|
||||
public async requestPermission(): Promise<NotificationPermission> {
|
||||
if (!this.isSupported()) {
|
||||
throw new Error('Push notifications are not supported')
|
||||
}
|
||||
return await Notification.requestPermission()
|
||||
}
|
||||
|
||||
public async registerServiceWorker(): Promise<ServiceWorkerRegistration> {
|
||||
if (!this.isSupported()) {
|
||||
throw new Error('Service workers are not supported')
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register(this.serviceWorkerPath)
|
||||
console.log('Service worker registered:', registration)
|
||||
|
||||
// Wait for service worker to be ready
|
||||
await navigator.serviceWorker.ready
|
||||
|
||||
return registration
|
||||
} catch (error) {
|
||||
console.error('Service worker registration failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async subscribe(): Promise<any> {
|
||||
const permission = await this.requestPermission()
|
||||
if (permission !== 'granted') {
|
||||
throw new Error('Notification permission not granted')
|
||||
}
|
||||
|
||||
const registration = await this.registerServiceWorker()
|
||||
|
||||
// For demo purposes, we'll simulate subscription without actual VAPID keys
|
||||
// In production, you would use real VAPID keys here
|
||||
try {
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
|
||||
})
|
||||
|
||||
console.log('Push subscription:', subscription)
|
||||
return {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: this.arrayBufferToBase64(subscription.getKey('p256dh')!),
|
||||
auth: this.arrayBufferToBase64(subscription.getKey('auth')!),
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Real push subscription failed, simulating for demo:', error)
|
||||
// Return simulated subscription for demo
|
||||
return {
|
||||
endpoint: 'demo-endpoint',
|
||||
keys: { p256dh: 'demo-key', auth: 'demo-auth' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async isSubscribed(): Promise<boolean> {
|
||||
if (!this.isSupported()) return false
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration()
|
||||
if (!registration) return false
|
||||
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
return subscription !== null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public async unsubscribe(): Promise<void> {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration()
|
||||
if (!registration) return
|
||||
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
if (subscription) {
|
||||
await subscription.unsubscribe()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unsubscribe failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
public async sendTestNotification(): Promise<void> {
|
||||
// Send a test notification using the service worker
|
||||
const registration = await navigator.serviceWorker.getRegistration()
|
||||
if (!registration) {
|
||||
throw new Error('Service worker not registered')
|
||||
}
|
||||
|
||||
// Simulate receiving a push message
|
||||
if (registration.active) {
|
||||
registration.active.postMessage({
|
||||
type: 'TEST_NOTIFICATION',
|
||||
payload: {
|
||||
title: 'Test Notification',
|
||||
body: 'This is a test notification from the demo!',
|
||||
icon: '/icons/notification-icon.png',
|
||||
badge: '/icons/notification-badge.png',
|
||||
data: {
|
||||
url: '/admin/collections/notifications',
|
||||
notificationId: 'demo-' + Date.now()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Also show a direct notification for testing
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification('Direct Test Notification', {
|
||||
body: 'This notification was sent directly from JavaScript',
|
||||
icon: '/icons/notification-icon.png',
|
||||
tag: 'direct-test'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
|
||||
const rawData = window.atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray
|
||||
}
|
||||
|
||||
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
const binary = Array.from(bytes).map(b => String.fromCharCode(b)).join('')
|
||||
return window.btoa(binary)
|
||||
}
|
||||
}
|
||||
|
||||
// Available channels (should match the configuration in payload.config.ts)
|
||||
const AVAILABLE_CHANNELS = [
|
||||
{ id: 'general', name: 'General Notifications', description: 'General updates and announcements', defaultEnabled: true },
|
||||
{ id: 'orders', name: 'Order Updates', description: 'Order status changes and shipping notifications', defaultEnabled: true },
|
||||
{ id: 'products', name: 'Product Updates', description: 'New products, restocks, and price changes', defaultEnabled: false },
|
||||
{ id: 'marketing', name: 'Marketing & Promotions', description: 'Special offers, sales, and promotional content', defaultEnabled: false },
|
||||
]
|
||||
|
||||
export default function DemoPage() {
|
||||
const [isSupported, setIsSupported] = useState(false)
|
||||
const [isSubscribed, setIsSubscribed] = useState(false)
|
||||
const [permission, setPermission] = useState<NotificationPermission>('default')
|
||||
const [pushManager, setPushManager] = useState<DemoClientPushManager | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedChannels, setSelectedChannels] = useState<string[]>(
|
||||
AVAILABLE_CHANNELS.filter(channel => channel.defaultEnabled).map(channel => channel.id)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Use the real VAPID public key from environment
|
||||
const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || 'BNde-uFUkQB5BweFbOt_40Tn3xZahMop2JKT8kqRn4UqMMinieguHmVCTxwN_qfM-jZ0YFpVpIk3CWehlXcTl8A'
|
||||
const manager = new DemoClientPushManager(vapidPublicKey)
|
||||
setPushManager(manager)
|
||||
setIsSupported(manager.isSupported())
|
||||
setPermission(manager.getPermissionStatus())
|
||||
|
||||
if (manager.isSupported()) {
|
||||
manager.isSubscribed().then(setIsSubscribed)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
if (!pushManager) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const subscription = await pushManager.subscribe()
|
||||
|
||||
// Save the subscription to Payload's database using the plugin's API endpoint
|
||||
const response = await fetch('/api/push-notifications/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription: subscription,
|
||||
user: 'customer@example.com', // Associate with the demo customer user
|
||||
userAgent: navigator.userAgent,
|
||||
channels: selectedChannels,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
setIsSubscribed(true)
|
||||
setPermission('granted')
|
||||
alert('Successfully subscribed to push notifications!\n\nSubscription saved to database.')
|
||||
} else {
|
||||
const error = await response.text()
|
||||
throw new Error(`Failed to save subscription: ${error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe:', error)
|
||||
alert('Failed to subscribe to push notifications: ' + (error as Error).message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleUnsubscribe = async () => {
|
||||
if (!pushManager) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await pushManager.unsubscribe()
|
||||
|
||||
// Remove the subscription from Payload's database
|
||||
const response = await fetch('/api/push-notifications/unsubscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user: 'customer@example.com', // Associate with the demo customer user
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
setIsSubscribed(false)
|
||||
alert('Successfully unsubscribed from push notifications')
|
||||
} else {
|
||||
const error = await response.text()
|
||||
console.warn('Failed to remove subscription from database:', error)
|
||||
setIsSubscribed(false)
|
||||
alert('Unsubscribed from browser, but may still be in database')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to unsubscribe:', error)
|
||||
alert('Failed to unsubscribe from push notifications: ' + (error as Error).message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleTestNotification = async () => {
|
||||
if (!pushManager) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await pushManager.sendTestNotification()
|
||||
alert('Test notification sent! Check your browser notifications.')
|
||||
} catch (error) {
|
||||
console.error('Failed to send test notification:', error)
|
||||
alert('Failed to send test notification: ' + (error as Error).message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h1>Payload Notifications Plugin Demo</h1>
|
||||
|
||||
<div style={{ marginBottom: '2rem', padding: '1rem', border: '1px solid #ccc', borderRadius: '8px' }}>
|
||||
<h2>🔔 Web Push Notifications</h2>
|
||||
|
||||
{!isSupported ? (
|
||||
<div style={{ color: 'red' }}>
|
||||
❌ Push notifications are not supported in this browser
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p><strong>Status:</strong> {isSubscribed ? '✅ Subscribed' : '❌ Not subscribed'}</p>
|
||||
<p><strong>Permission:</strong> {permission}</p>
|
||||
|
||||
{!isSubscribed && (
|
||||
<div style={{ marginTop: '1rem', marginBottom: '1rem' }}>
|
||||
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem' }}>📢 Select Notification Channels</h3>
|
||||
<p style={{ marginBottom: '1rem', fontSize: '0.9rem', color: '#666' }}>
|
||||
Choose which types of notifications you want to receive:
|
||||
</p>
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
{AVAILABLE_CHANNELS.map(channel => (
|
||||
<label
|
||||
key={channel.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '0.5rem',
|
||||
padding: '0.75rem',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: selectedChannels.includes(channel.id) ? '#f0f9ff' : '#fafafa',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedChannels.includes(channel.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedChannels(prev => [...prev, channel.id])
|
||||
} else {
|
||||
setSelectedChannels(prev => prev.filter(id => id !== channel.id))
|
||||
}
|
||||
}}
|
||||
style={{ marginTop: '0.2rem' }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontWeight: '500', marginBottom: '0.25rem' }}>
|
||||
{channel.name}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: '#666' }}>
|
||||
{channel.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1rem', display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
{!isSubscribed ? (
|
||||
<button
|
||||
onClick={handleSubscribe}
|
||||
disabled={loading || selectedChannels.length === 0}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: selectedChannels.length === 0 ? '#ccc' : '#007FFF',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading || selectedChannels.length === 0 ? 'not-allowed' : 'pointer',
|
||||
opacity: loading || selectedChannels.length === 0 ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Subscribing...' : selectedChannels.length === 0 ? 'Select at least one channel' : 'Enable Notifications'}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={handleUnsubscribe}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#dc3545',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Unsubscribing...' : 'Disable Notifications'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleTestNotification}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#28a745',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Sending...' : 'Send Test Notification'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '2rem', padding: '1rem', border: '1px solid #ccc', borderRadius: '8px' }}>
|
||||
<h2>📱 Admin Panel Features</h2>
|
||||
<p>The notifications plugin adds the following to your Payload admin panel:</p>
|
||||
<ul>
|
||||
<li><strong>Notifications Collection:</strong> Create and manage notifications with rich text content</li>
|
||||
<li><strong>Push Subscriptions Collection:</strong> View and manage user push notification subscriptions (check here after subscribing!)</li>
|
||||
<li><strong>📢 Channel-Based Subscriptions:</strong> Users can subscribe to specific notification channels (General, Orders, Products, Marketing)</li>
|
||||
<li><strong>Read/Unread Tracking:</strong> Monitor which notifications have been read</li>
|
||||
<li><strong>User Targeting:</strong> Send notifications to specific users</li>
|
||||
<li><strong>🎯 Automatic Push Notifications:</strong> Push notifications are sent automatically when notifications are created!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '2rem', padding: '1rem', border: '2px solid #28a745', borderRadius: '8px', backgroundColor: '#f8fff8' }}>
|
||||
<h2 style={{ color: '#28a745', marginBottom: '1rem' }}>🚀 Try Automatic Push Notifications</h2>
|
||||
<div style={{ padding: '0.75rem', backgroundColor: '#fff3cd', borderRadius: '4px', marginBottom: '1rem', border: '1px solid #ffeaa7' }}>
|
||||
<strong>⚠️ Important:</strong> You must be signed in to subscribe to push notifications. The subscription associates with your user account.
|
||||
</div>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Step 1:</strong> <a href="/admin/login" target="_blank" style={{ color: '#007FFF' }}>Sign in to the admin panel</a> first (dev@payloadcms.com / test)
|
||||
</p>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Step 2:</strong> Return here and subscribe to push notifications above ↑
|
||||
</p>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Step 3:</strong> Go to the <a href="/admin" target="_blank" style={{ color: '#007FFF' }}>admin panel</a> and create a new notification
|
||||
</p>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Step 4:</strong> Set the recipient to "customer@example.com" (the test user)
|
||||
</p>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Step 5:</strong> Choose a notification channel (General, Orders, Products, or Marketing) - must match your subscription
|
||||
</p>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Step 6:</strong> Save the notification and watch for an automatic push notification! 🎉
|
||||
</p>
|
||||
<div style={{ padding: '0.75rem', backgroundColor: '#e7f3ff', borderRadius: '4px', fontSize: '0.9rem' }}>
|
||||
<strong>💡 How it works:</strong> When you create a notification in the admin panel, the plugin automatically:
|
||||
<ul style={{ margin: '0.5rem 0', paddingLeft: '1.5rem' }}>
|
||||
<li>Extracts the title and message content</li>
|
||||
<li>Finds all push subscriptions for the recipient</li>
|
||||
<li>Sends push notifications to their devices</li>
|
||||
<li>Handles errors gracefully without breaking the notification creation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '2rem', padding: '1rem', border: '1px solid #ccc', borderRadius: '8px' }}>
|
||||
<h2>🚀 API Endpoints</h2>
|
||||
<p>The plugin automatically creates these API endpoints for web push:</p>
|
||||
<ul>
|
||||
<li><code>POST /api/push-notifications/subscribe</code> - Subscribe to push notifications</li>
|
||||
<li><code>POST /api/push-notifications/unsubscribe</code> - Unsubscribe from push notifications</li>
|
||||
<li><code>GET /api/push-notifications/vapid-public-key</code> - Get VAPID public key</li>
|
||||
<li><code>POST /api/push-notifications/send</code> - Send notification to user</li>
|
||||
<li><code>POST /api/push-notifications/test</code> - Send test notification (admin only)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '2rem', padding: '1rem', border: '1px solid #ccc', borderRadius: '8px' }}>
|
||||
<h2>💡 Getting Started</h2>
|
||||
<ol>
|
||||
<li>Generate VAPID keys: <code>npx web-push generate-vapid-keys</code></li>
|
||||
<li>Add the keys to your <code>.env</code> file</li>
|
||||
<li>Create a service worker at <code>/public/sw.js</code></li>
|
||||
<li>Use the client-side utilities to manage subscriptions</li>
|
||||
<li>Send notifications programmatically or via the admin panel</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '1rem', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<h3>📋 Sample Data</h3>
|
||||
<p>This demo includes:</p>
|
||||
<ul>
|
||||
<li>Sample users (admin and customer)</li>
|
||||
<li>Sample notifications demonstrating channels</li>
|
||||
<li>Push subscription management with channel filtering</li>
|
||||
<li>Automatic push notification hooks</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Login:</strong> dev@payloadcms.com / test<br/>
|
||||
<strong>Admin Panel:</strong> <a href="/admin" target="_blank">/admin</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
92
dev/app/(app)/layout.tsx
Normal file
92
dev/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
export default function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>
|
||||
<div style={{ minHeight: '100vh', backgroundColor: '#fafafa' }}>
|
||||
<nav style={{
|
||||
backgroundColor: 'white',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
padding: '1rem 2rem',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.5rem' }}>🔔</span>
|
||||
<span style={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||
Payload Notifications
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<a
|
||||
href="/"
|
||||
style={{
|
||||
color: '#007FFF',
|
||||
textDecoration: 'none',
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href="/demo"
|
||||
style={{
|
||||
color: '#007FFF',
|
||||
textDecoration: 'none',
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
Push Demo
|
||||
</a>
|
||||
<a
|
||||
href="/admin"
|
||||
style={{
|
||||
color: '#007FFF',
|
||||
textDecoration: 'none',
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #007FFF'
|
||||
}}
|
||||
>
|
||||
Admin Panel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<footer style={{
|
||||
marginTop: '4rem',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
backgroundColor: 'white',
|
||||
color: '#6b7280'
|
||||
}}>
|
||||
<p>
|
||||
🔔 Payload Notifications Plugin Demo |
|
||||
<a
|
||||
href="https://github.com/xtr-dev/payload-notifications"
|
||||
style={{ color: '#007FFF', marginLeft: '0.5rem' }}
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
126
dev/app/(app)/page.tsx
Normal file
126
dev/app/(app)/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '3rem' }}>
|
||||
<h1 style={{ fontSize: '2.5rem', marginBottom: '1rem' }}>
|
||||
🔔 Payload Notifications Plugin
|
||||
</h1>
|
||||
<p style={{ fontSize: '1.2rem', color: '#666' }}>
|
||||
A comprehensive demo of the @xtr-dev/payload-notifications plugin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1.5rem', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))' }}>
|
||||
<div style={{
|
||||
padding: '2rem',
|
||||
border: '2px solid #007FFF',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#f8f9ff'
|
||||
}}>
|
||||
<h2 style={{ marginBottom: '1rem', color: '#007FFF' }}>🛠️ Admin Panel</h2>
|
||||
<p style={{ marginBottom: '1.5rem' }}>
|
||||
Access the Payload admin panel to manage notifications, users, orders, and more.
|
||||
</p>
|
||||
<Link
|
||||
href="/admin"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#007FFF',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '6px',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Open Admin Panel
|
||||
</Link>
|
||||
<div style={{ marginTop: '1rem', fontSize: '0.9rem', color: '#666' }}>
|
||||
<strong>Login:</strong> dev@payloadcms.com / test
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
padding: '2rem',
|
||||
border: '2px solid #28a745',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#f8fff8'
|
||||
}}>
|
||||
<h2 style={{ marginBottom: '1rem', color: '#28a745' }}>📱 Push Notifications Demo</h2>
|
||||
<p style={{ marginBottom: '1.5rem' }}>
|
||||
Test the web push notification features and see how they work in a real application.
|
||||
</p>
|
||||
<Link
|
||||
href="/demo"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.75rem 1.5rem',
|
||||
backgroundColor: '#28a745',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '6px',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
View Demo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '3rem', padding: '2rem', backgroundColor: '#f8f9fa', borderRadius: '12px' }}>
|
||||
<h2 style={{ marginBottom: '1.5rem' }}>🚀 What's Included</h2>
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}>
|
||||
<div>
|
||||
<h3 style={{ color: '#007FFF', marginBottom: '0.5rem' }}>📧 Notifications Collection</h3>
|
||||
<p style={{ fontSize: '0.9rem', color: '#666' }}>
|
||||
Rich text notifications with read/unread tracking and recipient targeting
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#007FFF', marginBottom: '0.5rem' }}>🔗 Relationship Attachments</h3>
|
||||
<p style={{ fontSize: '0.9rem', color: '#666' }}>
|
||||
Link notifications to orders, products, posts, or any collection
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#007FFF', marginBottom: '0.5rem' }}>🔔 Web Push Support</h3>
|
||||
<p style={{ fontSize: '0.9rem', color: '#666' }}>
|
||||
VAPID-secured push notifications for mobile and desktop browsers
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#007FFF', marginBottom: '0.5rem' }}>⚙️ Configurable Access</h3>
|
||||
<p style={{ fontSize: '0.9rem', color: '#666' }}>
|
||||
Flexible access control with role-based permissions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '2rem', padding: '1.5rem', border: '1px solid #dee2e6', borderRadius: '8px' }}>
|
||||
<h3 style={{ marginBottom: '1rem' }}>📋 Sample Data</h3>
|
||||
<p style={{ marginBottom: '1rem' }}>This demo environment includes:</p>
|
||||
<ul style={{ marginLeft: '1.5rem', color: '#666' }}>
|
||||
<li>Admin user (dev@payloadcms.com) and customer user</li>
|
||||
<li>Sample products (headphones, t-shirt, JavaScript guide)</li>
|
||||
<li>Sample orders with different statuses</li>
|
||||
<li>Sample notifications with relationship attachments</li>
|
||||
<li>Push subscription management</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '2rem', textAlign: 'center', color: '#666' }}>
|
||||
<p>
|
||||
📖 <a href="https://github.com/xtr-dev/payload-notifications" style={{ color: '#007FFF' }}>
|
||||
View Documentation
|
||||
</a> |
|
||||
🐙 <a href="https://github.com/xtr-dev/payload-notifications" style={{ color: '#007FFF' }}>
|
||||
GitHub Repository
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
dev/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal file
25
dev/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
25
dev/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
25
dev/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default Page
|
||||
49
dev/app/(payload)/admin/importMap.js
Normal file
49
dev/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
|
||||
export const importMap = {
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
|
||||
}
|
||||
19
dev/app/(payload)/api/[...slug]/route.ts
Normal file
19
dev/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const PUT = REST_PUT(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
7
dev/app/(payload)/api/graphql-playground/route.ts
Normal file
7
dev/app/(payload)/api/graphql-playground/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
8
dev/app/(payload)/api/graphql/route.ts
Normal file
8
dev/app/(payload)/api/graphql/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
0
dev/app/(payload)/custom.scss
Normal file
0
dev/app/(payload)/custom.scss
Normal file
32
dev/app/(payload)/layout.tsx
Normal file
32
dev/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
14
dev/app/layout.tsx
Normal file
14
dev/app/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Payload Notifications Plugin Demo',
|
||||
description: 'Demo environment for the @xtr-dev/payload-notifications plugin',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
12
dev/app/my-route/route.ts
Normal file
12
dev/app/my-route/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async (request: Request) => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
message: 'This is an example of a custom route.',
|
||||
})
|
||||
}
|
||||
15
dev/e2e.spec.ts
Normal file
15
dev/e2e.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
// this is an example Playwright e2e test
|
||||
test('should render admin panel logo', async ({ page }) => {
|
||||
await page.goto('/admin')
|
||||
|
||||
// login
|
||||
await page.fill('#field-email', 'dev@payloadcms.com')
|
||||
await page.fill('#field-password', 'test')
|
||||
await page.click('.form-submit button')
|
||||
|
||||
// should show dashboard
|
||||
await expect(page).toHaveTitle(/Dashboard/)
|
||||
await expect(page.locator('.graphic-icon')).toBeVisible()
|
||||
})
|
||||
4
dev/helpers/credentials.ts
Normal file
4
dev/helpers/credentials.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const devUser = {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
}
|
||||
38
dev/helpers/testEmailAdapter.ts
Normal file
38
dev/helpers/testEmailAdapter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { EmailAdapter, SendEmailOptions } from 'payload'
|
||||
|
||||
/**
|
||||
* Logs all emails to stdout
|
||||
*/
|
||||
export const testEmailAdapter: EmailAdapter<void> = ({ payload }) => ({
|
||||
name: 'test-email-adapter',
|
||||
defaultFromAddress: 'dev@payloadcms.com',
|
||||
defaultFromName: 'Payload Test',
|
||||
sendEmail: async (message) => {
|
||||
const stringifiedTo = getStringifiedToAddress(message)
|
||||
const res = `Test email to: '${stringifiedTo}', Subject: '${message.subject}'`
|
||||
payload.logger.info({ content: message, msg: res })
|
||||
return Promise.resolve()
|
||||
},
|
||||
})
|
||||
|
||||
function getStringifiedToAddress(message: SendEmailOptions): string | undefined {
|
||||
let stringifiedTo: string | undefined
|
||||
|
||||
if (typeof message.to === 'string') {
|
||||
stringifiedTo = message.to
|
||||
} else if (Array.isArray(message.to)) {
|
||||
stringifiedTo = message.to
|
||||
.map((to: { address: string } | string) => {
|
||||
if (typeof to === 'string') {
|
||||
return to
|
||||
} else if (to.address) {
|
||||
return to.address
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.join(', ')
|
||||
} else if (message.to?.address) {
|
||||
stringifiedTo = message.to.address
|
||||
}
|
||||
return stringifiedTo
|
||||
}
|
||||
52
dev/int.spec.ts
Normal file
52
dev/int.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { createPayloadRequest, getPayload } from 'payload'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { customEndpointHandler } from '../src/endpoints/customEndpointHandler.js'
|
||||
|
||||
let payload: Payload
|
||||
|
||||
afterAll(async () => {
|
||||
await payload.destroy()
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
payload = await getPayload({ config })
|
||||
})
|
||||
|
||||
describe('Plugin integration tests', () => {
|
||||
test('should query custom endpoint added by plugin', async () => {
|
||||
const request = new Request('http://localhost:3000/api/my-plugin-endpoint', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const payloadRequest = await createPayloadRequest({ config, request })
|
||||
const response = await customEndpointHandler(payloadRequest)
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toMatchObject({
|
||||
message: 'Hello from custom endpoint',
|
||||
})
|
||||
})
|
||||
|
||||
test('can create post with custom text field added by plugin', async () => {
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
addedByPlugin: 'added by plugin',
|
||||
},
|
||||
})
|
||||
expect(post.addedByPlugin).toBe('added by plugin')
|
||||
})
|
||||
|
||||
test('plugin creates and seeds plugin-collection', async () => {
|
||||
expect(payload.collections['plugin-collection']).toBeDefined()
|
||||
|
||||
const { docs } = await payload.find({ collection: 'plugin-collection' })
|
||||
|
||||
expect(docs).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
5
dev/next-env.d.ts
vendored
Normal file
5
dev/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
21
dev/next.config.mjs
Normal file
21
dev/next.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'path'
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
serverExternalPackages: ['mongodb-memory-server'],
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||
531
dev/payload-types.ts
Normal file
531
dev/payload-types.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
orders: Order;
|
||||
products: Product;
|
||||
posts: Post;
|
||||
media: Media;
|
||||
notifications: Notification;
|
||||
'push-subscriptions': PushSubscription;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
orders: OrdersSelect<false> | OrdersSelect<true>;
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
notifications: NotificationsSelect<false> | NotificationsSelect<true>;
|
||||
'push-subscriptions': PushSubscriptionsSelect<false> | PushSubscriptionsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
role: 'admin' | 'customer';
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orders".
|
||||
*/
|
||||
export interface Order {
|
||||
id: string;
|
||||
orderNumber: string;
|
||||
customer: string | User;
|
||||
status?: ('pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled') | null;
|
||||
total: number;
|
||||
products?: (string | Product)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "products".
|
||||
*/
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
price: number;
|
||||
category?: ('electronics' | 'clothing' | 'books' | 'home-garden') | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
author?: (string | null) | User;
|
||||
publishedAt?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* Manage user notifications and messaging
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "notifications".
|
||||
*/
|
||||
export interface Notification {
|
||||
id: string;
|
||||
/**
|
||||
* The notification title that will be displayed to users
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The notification message content
|
||||
*/
|
||||
message: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* The user who should receive this notification
|
||||
*/
|
||||
recipient: string | User;
|
||||
/**
|
||||
* Whether this notification has been read by the recipient
|
||||
*/
|
||||
isRead?: boolean | null;
|
||||
/**
|
||||
* When this notification was marked as read
|
||||
*/
|
||||
readAt?: string | null;
|
||||
attachments?: {
|
||||
order?: (string | null) | Order;
|
||||
product?: (string | Product)[] | null;
|
||||
post?: (string | null) | Post;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Web push notification subscriptions for users
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "push-subscriptions".
|
||||
*/
|
||||
export interface PushSubscription {
|
||||
id: string;
|
||||
/**
|
||||
* The user this push subscription belongs to
|
||||
*/
|
||||
user: string | User;
|
||||
/**
|
||||
* Push service endpoint URL
|
||||
*/
|
||||
endpoint: string;
|
||||
/**
|
||||
* User agent public key for encryption
|
||||
*/
|
||||
p256dh: string;
|
||||
/**
|
||||
* User agent authentication secret
|
||||
*/
|
||||
auth: string;
|
||||
/**
|
||||
* Browser/device information
|
||||
*/
|
||||
userAgent?: string | null;
|
||||
/**
|
||||
* Whether this subscription is still active
|
||||
*/
|
||||
isActive?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'orders';
|
||||
value: string | Order;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'products';
|
||||
value: string | Product;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'notifications';
|
||||
value: string | Notification;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'push-subscriptions';
|
||||
value: string | PushSubscription;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
role?: T;
|
||||
firstName?: T;
|
||||
lastName?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orders_select".
|
||||
*/
|
||||
export interface OrdersSelect<T extends boolean = true> {
|
||||
orderNumber?: T;
|
||||
customer?: T;
|
||||
status?: T;
|
||||
total?: T;
|
||||
products?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "products_select".
|
||||
*/
|
||||
export interface ProductsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
description?: T;
|
||||
price?: T;
|
||||
category?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
content?: T;
|
||||
author?: T;
|
||||
publishedAt?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
alt?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "notifications_select".
|
||||
*/
|
||||
export interface NotificationsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
message?: T;
|
||||
recipient?: T;
|
||||
isRead?: T;
|
||||
readAt?: T;
|
||||
attachments?:
|
||||
| T
|
||||
| {
|
||||
order?: T;
|
||||
product?: T;
|
||||
post?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "push-subscriptions_select".
|
||||
*/
|
||||
export interface PushSubscriptionsSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
endpoint?: T;
|
||||
p256dh?: T;
|
||||
auth?: T;
|
||||
userAgent?: T;
|
||||
isActive?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
176
dev/payload.config.ts
Normal file
176
dev/payload.config.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import sharp from 'sharp'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { testEmailAdapter } from './helpers/testEmailAdapter.ts'
|
||||
import { seed } from './seed.ts'
|
||||
import { notificationsPlugin } from '@xtr-dev/payload-notifications'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
if (!process.env.ROOT_DIR) {
|
||||
process.env.ROOT_DIR = dirname
|
||||
}
|
||||
|
||||
const buildConfigWithMemoryDB = async () => {
|
||||
if (!process.env.DATABASE_URI) {
|
||||
// Use a simple memory server instead of replica set for better stability
|
||||
const { MongoMemoryServer } = await import('mongodb-memory-server')
|
||||
const memoryDB = await MongoMemoryServer.create({
|
||||
instance: {
|
||||
dbName: 'payloadmemory',
|
||||
},
|
||||
})
|
||||
|
||||
process.env.DATABASE_URI = memoryDB.getUri()
|
||||
}
|
||||
|
||||
return buildConfig({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
// Users collection with roles for authentication
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'role',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Admin', value: 'admin' },
|
||||
{ label: 'Customer', value: 'customer' },
|
||||
],
|
||||
defaultValue: 'customer',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'firstName',
|
||||
type: 'text',
|
||||
label: 'First Name',
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
type: 'text',
|
||||
label: 'Last Name',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
db: mongooseAdapter({
|
||||
ensureIndexes: true,
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
editor: lexicalEditor(),
|
||||
email: testEmailAdapter,
|
||||
onInit: async (payload) => {
|
||||
await seed(payload)
|
||||
},
|
||||
plugins: [
|
||||
// Demo of the notifications plugin with relationships and channels
|
||||
notificationsPlugin({
|
||||
collections: {
|
||||
slug: 'notifications',
|
||||
labels: {
|
||||
singular: 'Notification',
|
||||
plural: 'Notifications'
|
||||
}
|
||||
},
|
||||
channels: [
|
||||
{
|
||||
id: 'general',
|
||||
name: 'General Notifications',
|
||||
description: 'General updates and announcements',
|
||||
defaultEnabled: true
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
name: 'Order Updates',
|
||||
description: 'Order status changes and shipping notifications',
|
||||
defaultEnabled: true
|
||||
},
|
||||
{
|
||||
id: 'products',
|
||||
name: 'Product Updates',
|
||||
description: 'New products, restocks, and price changes',
|
||||
defaultEnabled: false
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
name: 'Marketing & Promotions',
|
||||
description: 'Special offers, sales, and promotional content',
|
||||
defaultEnabled: false
|
||||
}
|
||||
],
|
||||
access: {
|
||||
read: ({ req }: { req: any }) => Boolean(req.user),
|
||||
create: ({ req }: { req: any }) => Boolean(req.user),
|
||||
update: ({ req }: { req: any }) => Boolean(req.user),
|
||||
delete: ({ req }: { req: any }) => Boolean(req.user?.role === 'admin'),
|
||||
},
|
||||
webPush: {
|
||||
enabled: true,
|
||||
autoPush: true, // Enable automatic push notifications
|
||||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY || 'BMrF5MbHcaEo6w4lPjG9m3BvONvFPfz7jLJ9t0F9yJGzSI3ZUHQj9fNUP7w2D8h1kI4x3YzJ1a4f0nS5g6t2F9L',
|
||||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY || 'your-private-key-here',
|
||||
vapidSubject: 'mailto:test@example.com',
|
||||
// Custom notification transformer for demo
|
||||
transformNotification: (notification: any) => {
|
||||
const title = notification.title || 'New Notification'
|
||||
|
||||
// Extract text from rich text message
|
||||
let body = 'You have a new notification'
|
||||
if (notification.message && Array.isArray(notification.message)) {
|
||||
const textParts: string[] = []
|
||||
notification.message.forEach((block: any) => {
|
||||
if (block.children && Array.isArray(block.children)) {
|
||||
block.children.forEach((child: any) => {
|
||||
if (child.text) textParts.push(child.text)
|
||||
})
|
||||
}
|
||||
})
|
||||
if (textParts.length > 0) {
|
||||
body = textParts.join(' ').substring(0, 120) + (textParts.join(' ').length > 120 ? '...' : '')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `🔔 ${title}`,
|
||||
body,
|
||||
icon: '/icons/notification-icon.png',
|
||||
badge: '/icons/notification-badge.png',
|
||||
data: {
|
||||
notificationId: notification.id,
|
||||
url: `/admin/collections/notifications/${notification.id}`,
|
||||
createdAt: notification.createdAt,
|
||||
},
|
||||
actions: [
|
||||
{ action: 'view', title: 'View in Admin', icon: '/icons/view.png' },
|
||||
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' }
|
||||
],
|
||||
tag: `notification-${notification.id}`,
|
||||
requireInteraction: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
||||
sharp,
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default buildConfigWithMemoryDB()
|
||||
27
dev/public/icons/README.md
Normal file
27
dev/public/icons/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Notification Icons
|
||||
|
||||
This directory contains icons for web push notifications.
|
||||
|
||||
## Required Icons:
|
||||
|
||||
- `notification-icon.png` - Main notification icon (recommended: 192x192px)
|
||||
- `notification-badge.png` - Small badge icon (recommended: 72x72px)
|
||||
- `view.png` - View action icon (recommended: 32x32px)
|
||||
- `dismiss.png` - Dismiss action icon (recommended: 32x32px)
|
||||
|
||||
## Icon Requirements:
|
||||
|
||||
1. **Format**: PNG with transparency support
|
||||
2. **Size**: Multiple sizes recommended (72x72, 96x96, 128x128, 192x192, 256x256, 512x512)
|
||||
3. **Design**: Simple, clear, recognizable at small sizes
|
||||
4. **Background**: Transparent or solid color that works on any background
|
||||
|
||||
## Fallback:
|
||||
|
||||
If custom icons are not provided, the service worker will use these default paths:
|
||||
- `/icons/notification-icon.png`
|
||||
- `/icons/notification-badge.png`
|
||||
- `/icons/view.png`
|
||||
- `/icons/dismiss.png`
|
||||
|
||||
You can create simple colored PNG files or use emoji-based icons for testing.
|
||||
197
dev/public/sw.js
Normal file
197
dev/public/sw.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Service Worker for Web Push Notifications
|
||||
* Payload Notifications Plugin Demo
|
||||
*/
|
||||
|
||||
console.log('[SW] Service worker loaded')
|
||||
|
||||
// Service worker lifecycle events
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Installing service worker')
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating service worker')
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
// Handle push events
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('[SW] Push event received')
|
||||
|
||||
if (!event.data) {
|
||||
console.log('[SW] Push event has no data')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = event.data.json()
|
||||
console.log('[SW] Push payload:', payload)
|
||||
|
||||
const { title, body, icon, badge, image, data, actions, tag, requireInteraction } = payload
|
||||
|
||||
const notificationOptions = {
|
||||
body,
|
||||
icon: icon || '/icons/notification-icon.png',
|
||||
badge: badge || '/icons/notification-badge.png',
|
||||
image,
|
||||
data,
|
||||
actions: actions || [
|
||||
{ action: 'view', title: 'View', icon: '/icons/view.png' },
|
||||
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' }
|
||||
],
|
||||
tag: tag || 'notification',
|
||||
requireInteraction: requireInteraction || false,
|
||||
timestamp: Date.now(),
|
||||
vibrate: [200, 100, 200],
|
||||
renotify: true,
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title || 'New Notification', notificationOptions)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[SW] Error processing push notification:', error)
|
||||
|
||||
// Fallback notification
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('New Notification', {
|
||||
body: 'You have a new notification',
|
||||
icon: '/icons/notification-icon.png',
|
||||
badge: '/icons/notification-badge.png',
|
||||
tag: 'fallback',
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle notification clicks
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
console.log('[SW] Notification click received')
|
||||
console.log('[SW] Action:', event.action)
|
||||
console.log('[SW] Notification data:', event.notification.data)
|
||||
|
||||
event.notification.close()
|
||||
|
||||
const data = event.notification.data || {}
|
||||
|
||||
// Handle action button clicks
|
||||
if (event.action) {
|
||||
switch (event.action) {
|
||||
case 'view':
|
||||
if (data.url) {
|
||||
event.waitUntil(
|
||||
clients.openWindow(data.url)
|
||||
)
|
||||
} else {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/admin/collections/notifications')
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'dismiss':
|
||||
// Just close the notification (already done above)
|
||||
break
|
||||
default:
|
||||
console.log('[SW] Unknown action:', event.action)
|
||||
}
|
||||
} else {
|
||||
// Default click behavior - open the admin panel or specific URL
|
||||
const urlToOpen = data.url || '/admin/collections/notifications'
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then((windowClients) => {
|
||||
// Check if there is already an open window
|
||||
for (const client of windowClients) {
|
||||
if (client.url.includes('/admin') && 'focus' in client) {
|
||||
return client.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// If no admin window is open, open a new one
|
||||
return clients.openWindow(urlToOpen)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Track notification click
|
||||
if (data.notificationId) {
|
||||
fetch('/api/push-notifications/track', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'click',
|
||||
notificationId: data.notificationId,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch((error) => {
|
||||
console.error('[SW] Failed to track notification click:', error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Handle notification close events
|
||||
self.addEventListener('notificationclose', (event) => {
|
||||
console.log('[SW] Notification closed:', event.notification.tag)
|
||||
|
||||
const data = event.notification.data || {}
|
||||
|
||||
// Track notification close
|
||||
if (data.notificationId) {
|
||||
fetch('/api/push-notifications/track', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'close',
|
||||
notificationId: data.notificationId,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch((error) => {
|
||||
console.error('[SW] Failed to track notification close:', error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Handle background sync (optional)
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[SW] Background sync:', event.tag)
|
||||
|
||||
if (event.tag === 'push-notification-sync') {
|
||||
event.waitUntil(
|
||||
// Handle offline notification sync
|
||||
Promise.resolve()
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle message events from the main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('[SW] Message received:', event.data)
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
|
||||
// Handle test notifications sent from the demo page
|
||||
if (event.data && event.data.type === 'TEST_NOTIFICATION') {
|
||||
const payload = event.data.payload
|
||||
|
||||
self.registration.showNotification(payload.title, {
|
||||
body: payload.body,
|
||||
icon: payload.icon,
|
||||
badge: payload.badge,
|
||||
data: payload.data,
|
||||
actions: [
|
||||
{ action: 'view', title: 'View', icon: '/icons/view.png' },
|
||||
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' }
|
||||
],
|
||||
tag: 'test-notification',
|
||||
requireInteraction: false,
|
||||
timestamp: Date.now(),
|
||||
vibrate: [200, 100, 200],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
console.log('[SW] Service worker setup complete')
|
||||
183
dev/seed.ts
Normal file
183
dev/seed.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { devUser } from './helpers/credentials.ts'
|
||||
|
||||
export const seed = async (payload: Payload) => {
|
||||
console.log('Seeding database...')
|
||||
|
||||
// Check if admin user exists
|
||||
const { totalDocs } = await payload.count({
|
||||
collection: 'users',
|
||||
where: {
|
||||
email: {
|
||||
equals: devUser.email,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let adminUser: any
|
||||
let customerUser: any
|
||||
|
||||
if (!totalDocs) {
|
||||
// Create admin user
|
||||
adminUser = await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
...devUser,
|
||||
role: 'admin',
|
||||
firstName: 'Admin',
|
||||
lastName: 'User',
|
||||
},
|
||||
})
|
||||
console.log('✅ Created admin user:', devUser.email)
|
||||
|
||||
// Create sample customer user
|
||||
customerUser = await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'customer@example.com',
|
||||
password: 'test',
|
||||
role: 'customer',
|
||||
firstName: 'John',
|
||||
lastName: 'Customer',
|
||||
},
|
||||
})
|
||||
console.log('✅ Created customer user')
|
||||
} else {
|
||||
// Get existing users
|
||||
const existingAdmin = await payload.find({
|
||||
collection: 'users',
|
||||
where: { email: { equals: devUser.email } },
|
||||
limit: 1,
|
||||
})
|
||||
adminUser = existingAdmin.docs[0]
|
||||
|
||||
const existingCustomer = await payload.find({
|
||||
collection: 'users',
|
||||
where: { email: { equals: 'customer@example.com' } },
|
||||
limit: 1,
|
||||
})
|
||||
customerUser = existingCustomer.docs[0]
|
||||
}
|
||||
|
||||
// Check if sample notifications already exist
|
||||
const existingNotifications = await payload.count({ collection: 'notifications' })
|
||||
|
||||
if (existingNotifications.totalDocs === 0) {
|
||||
|
||||
// Create sample notifications
|
||||
await payload.create({
|
||||
collection: 'notifications',
|
||||
data: {
|
||||
title: 'Welcome to the Demo!',
|
||||
message: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Welcome to the notifications plugin demo! This notification was created during the seeding process. Try creating your own notifications and watch the automatic push notifications work.',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
recipient: customerUser.id,
|
||||
channel: 'general',
|
||||
isRead: false,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'notifications',
|
||||
data: {
|
||||
title: 'Orders Channel Demo',
|
||||
message: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'This is a sample notification for the Orders channel. Users subscribed to the Orders channel will receive notifications like this one.',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
recipient: customerUser.id,
|
||||
channel: 'orders',
|
||||
isRead: false,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'notifications',
|
||||
data: {
|
||||
title: 'New Product Recommendation',
|
||||
message: {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'This is a sample notification for the Products channel. This notification has been marked as read to demonstrate the read/unread functionality.',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
recipient: customerUser.id,
|
||||
channel: 'products',
|
||||
isRead: true,
|
||||
readAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Created sample notifications')
|
||||
console.log('\n🎉 Database seeded successfully!')
|
||||
console.log('\n📝 You can now:')
|
||||
console.log(' • Login as admin: dev@payloadcms.com / test')
|
||||
console.log(' • View notifications in the admin panel')
|
||||
console.log(' • Create new notifications and watch automatic push notifications!')
|
||||
console.log(' • Test the channel-based subscription system')
|
||||
console.log(' • Try the demo at /demo to subscribe to push notifications')
|
||||
} else {
|
||||
console.log('✅ Sample data already exists, skipping seed')
|
||||
}
|
||||
}
|
||||
43
dev/tsconfig.json
Normal file
43
dev/tsconfig.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": [],
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.mjs",
|
||||
"**/*.cjs",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next.config.mjs",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"rootDir": "./",
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./payload.config.ts"
|
||||
],
|
||||
"@xtr-dev/payload-notifications": [
|
||||
"../src/index.ts"
|
||||
],
|
||||
"@xtr-dev/payload-notifications/client": [
|
||||
"../src/exports/client.ts"
|
||||
],
|
||||
"@xtr-dev/payload-notifications/rsc": [
|
||||
"../src/exports/rsc.ts"
|
||||
]
|
||||
},
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"allowJs": true,
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user