mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 16:23:23 +00:00
Migrate dev server from MongoDB to SQLite
- Replace mongooseAdapter with sqliteAdapter in payload config - Update database configuration to use file:./dev.db - Remove MongoDB memory database helper and references - Simplify start script by removing verbose logging and MongoDB messaging - Fix email processing with immediate send support and proper queue handling - Restructure app with route groups for frontend/admin separation - Add dashboard and test pages for email management - Update API routes for improved email processing and testing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
310
dev/app/(frontend)/dashboard/page.tsx
Normal file
310
dev/app/(frontend)/dashboard/page.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface EmailStats {
|
||||
total: number
|
||||
sent: number
|
||||
pending: number
|
||||
failed: number
|
||||
processing: number
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [emailStats, setEmailStats] = useState<EmailStats>({
|
||||
total: 0,
|
||||
sent: 0,
|
||||
pending: 0,
|
||||
failed: 0,
|
||||
processing: 0
|
||||
})
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchEmailStats()
|
||||
}, [])
|
||||
|
||||
const fetchEmailStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/test-email')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.outbox?.emails) {
|
||||
const emails = data.outbox.emails
|
||||
const stats: EmailStats = {
|
||||
total: emails.length,
|
||||
sent: emails.filter((email: any) => email.status === 'sent').length,
|
||||
pending: emails.filter((email: any) => email.status === 'pending').length,
|
||||
failed: emails.filter((email: any) => email.status === 'failed').length,
|
||||
processing: emails.filter((email: any) => email.status === 'processing').length
|
||||
}
|
||||
setEmailStats(stats)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching email statistics:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const StatCard = ({ label, value, color, description }: { label: string; value: number; color: string; description: string }) => (
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '3rem',
|
||||
fontWeight: 'bold',
|
||||
color: color,
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
{value}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.875rem',
|
||||
color: '#6b7280'
|
||||
}}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: '#f9fafb',
|
||||
padding: '40px 20px',
|
||||
minHeight: 'calc(100vh - 80px)'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '48px' }}>
|
||||
<h1 style={{
|
||||
fontSize: '3rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#1f2937',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
📧 PayloadCMS Mailing Plugin
|
||||
</h1>
|
||||
<p style={{
|
||||
fontSize: '1.25rem',
|
||||
color: '#6b7280',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
Development Dashboard
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<Link
|
||||
href="/admin"
|
||||
style={{
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '8px',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
>
|
||||
📊 Admin Panel
|
||||
</Link>
|
||||
<Link
|
||||
href="/mailing-test"
|
||||
style={{
|
||||
backgroundColor: '#10b981',
|
||||
color: 'white',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '8px',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
>
|
||||
🧪 Test Interface
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Statistics */}
|
||||
<div style={{ marginBottom: '48px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#1f2937'
|
||||
}}>
|
||||
Email Statistics
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchEmailStats}
|
||||
disabled={loading}
|
||||
style={{
|
||||
backgroundColor: loading ? '#9ca3af' : '#6b7280',
|
||||
color: 'white',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '48px' }}>
|
||||
<div style={{ color: '#6b7280', fontSize: '1.1rem' }}>Loading email statistics...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: '24px'
|
||||
}}>
|
||||
<StatCard
|
||||
label="Total Emails"
|
||||
value={emailStats.total}
|
||||
color="#1f2937"
|
||||
description="All emails in the system"
|
||||
/>
|
||||
<StatCard
|
||||
label="Successfully Sent"
|
||||
value={emailStats.sent}
|
||||
color="#10b981"
|
||||
description="Delivered successfully"
|
||||
/>
|
||||
<StatCard
|
||||
label="Pending"
|
||||
value={emailStats.pending}
|
||||
color="#f59e0b"
|
||||
description="Waiting to be sent"
|
||||
/>
|
||||
<StatCard
|
||||
label="Failed"
|
||||
value={emailStats.failed}
|
||||
color="#ef4444"
|
||||
description="Failed to send"
|
||||
/>
|
||||
{emailStats.processing > 0 && (
|
||||
<StatCard
|
||||
label="Processing"
|
||||
value={emailStats.processing}
|
||||
color="#3b82f6"
|
||||
description="Currently being sent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '32px',
|
||||
border: '1px solid #e5e7eb',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<h3 style={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#1f2937',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
|
||||
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>🎯 Test Email Sending</h4>
|
||||
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
|
||||
Send test emails using templates with the interactive testing interface.
|
||||
</p>
|
||||
<Link
|
||||
href="/mailing-test"
|
||||
style={{
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Open Test Interface →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
|
||||
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>📝 Manage Templates</h4>
|
||||
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
|
||||
Create and edit email templates in the Payload admin interface.
|
||||
</p>
|
||||
<Link
|
||||
href="/admin/collections/email-templates"
|
||||
style={{
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Manage Templates →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
|
||||
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>📬 Email Queue</h4>
|
||||
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
|
||||
View and manage the email outbox and delivery status.
|
||||
</p>
|
||||
<Link
|
||||
href="/admin/collections/emails"
|
||||
style={{
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
View Email Queue →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '48px',
|
||||
padding: '24px',
|
||||
color: '#6b7280',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
PayloadCMS Mailing Plugin Development Environment
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
24
dev/app/(frontend)/layout.tsx
Normal file
24
dev/app/(frontend)/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'PayloadCMS Mailing Plugin - Development',
|
||||
description: 'Development environment for PayloadCMS Mailing Plugin',
|
||||
}
|
||||
|
||||
export default function FrontendLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
}}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -33,6 +33,8 @@ export default function MailingTestPage() {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
||||
const [toEmail, setToEmail] = useState<string>('test@example.com')
|
||||
const [variables, setVariables] = useState<Record<string, any>>({})
|
||||
const [jsonVariables, setJsonVariables] = useState<string>('{}')
|
||||
const [jsonError, setJsonError] = useState<string>('')
|
||||
const [emailType, setEmailType] = useState<'send' | 'schedule'>('send')
|
||||
const [scheduleDate, setScheduleDate] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -58,6 +60,23 @@ export default function MailingTestPage() {
|
||||
const template = templates.find(t => t.slug === templateSlug)
|
||||
if (template?.previewData) {
|
||||
setVariables(template.previewData)
|
||||
setJsonVariables(JSON.stringify(template.previewData, null, 2))
|
||||
} else {
|
||||
setVariables({})
|
||||
setJsonVariables('{}')
|
||||
}
|
||||
setJsonError('')
|
||||
}
|
||||
|
||||
const handleJsonVariablesChange = (jsonString: string) => {
|
||||
setJsonVariables(jsonString)
|
||||
setJsonError('')
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString)
|
||||
setVariables(parsed)
|
||||
} catch (error) {
|
||||
setJsonError(error instanceof Error ? error.message : 'Invalid JSON')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +86,11 @@ export default function MailingTestPage() {
|
||||
return
|
||||
}
|
||||
|
||||
if (jsonError) {
|
||||
setMessage('Please fix the JSON syntax error before sending')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
|
||||
@@ -88,7 +112,8 @@ export default function MailingTestPage() {
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
setMessage(`✅ ${result.message} (ID: ${result.emailId})`)
|
||||
const statusIcon = result.status === 'sent' ? '📧' : '📫'
|
||||
setMessage(`✅ ${statusIcon} ${result.message} (ID: ${result.emailId})`)
|
||||
fetchData() // Refresh email queue
|
||||
} else {
|
||||
setMessage(`❌ Error: ${result.error}`)
|
||||
@@ -204,28 +229,43 @@ export default function MailingTestPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTemplateData?.variables && (
|
||||
{selectedTemplate && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<h3>Template Variables:</h3>
|
||||
{selectedTemplateData.variables.map(variable => (
|
||||
<div key={variable.name} style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{variable.name} {variable.required && <span style={{ color: 'red' }}>*</span>}
|
||||
{variable.description && <small style={{ color: '#666' }}> - {variable.description}</small>}
|
||||
</label>
|
||||
<input
|
||||
type={variable.type === 'number' ? 'number' : variable.type === 'date' ? 'datetime-local' : 'text'}
|
||||
value={variables[variable.name] || ''}
|
||||
onChange={(e) => setVariables({
|
||||
...variables,
|
||||
[variable.name]: variable.type === 'number' ? Number(e.target.value) :
|
||||
variable.type === 'boolean' ? e.target.checked :
|
||||
e.target.value
|
||||
})}
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<strong>Template Variables (JSON):</strong>
|
||||
{selectedTemplateData?.variables && (
|
||||
<small style={{ color: '#666', marginLeft: '8px' }}>
|
||||
Available variables: {selectedTemplateData.variables.map(v => v.name).join(', ')}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
<textarea
|
||||
value={jsonVariables}
|
||||
onChange={(e) => handleJsonVariablesChange(e.target.value)}
|
||||
placeholder='{\n "firstName": "John",\n "siteName": "MyApp",\n "createdAt": "2023-01-01T00:00:00Z"\n}'
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '150px',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: jsonError ? '2px solid #dc3545' : '1px solid #ddd',
|
||||
fontFamily: 'monaco, "Courier New", monospace',
|
||||
fontSize: '13px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
{jsonError && (
|
||||
<div style={{
|
||||
color: '#dc3545',
|
||||
fontSize: '12px',
|
||||
marginTop: '5px',
|
||||
padding: '5px',
|
||||
backgroundColor: '#f8d7da',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Invalid JSON: {jsonError}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,24 +4,25 @@ import config from '@payload-config'
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Queue the combined email queue processing job
|
||||
const job = await payload.jobs.queue({
|
||||
task: 'process-email-queue',
|
||||
input: {},
|
||||
|
||||
// Run jobs in the default queue (the plugin already schedules email processing on init)
|
||||
const results = await payload.jobs.run({
|
||||
queue: 'default',
|
||||
})
|
||||
|
||||
|
||||
const processedCount = Array.isArray(results) ? results.length : (results ? 1 : 0)
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: 'Email queue processing job queued successfully (will process both pending and failed emails)',
|
||||
jobId: job.id,
|
||||
message: `Email queue processing completed. Processed ${processedCount} jobs.`,
|
||||
processedJobs: processedCount,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Process emails error:', error)
|
||||
return Response.json(
|
||||
{
|
||||
{
|
||||
error: 'Failed to process emails',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { sendEmail } from '@xtr-dev/payload-mailing'
|
||||
import { sendEmail, processEmailById } from '@xtr-dev/payload-mailing'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
@@ -41,10 +41,33 @@ export async function POST(request: Request) {
|
||||
|
||||
const result = await sendEmail(payload, emailOptions)
|
||||
|
||||
// If it's "send now" (not scheduled), process the email immediately
|
||||
if (type === 'send' && !scheduledAt) {
|
||||
try {
|
||||
await processEmailById(payload, String(result.id))
|
||||
return Response.json({
|
||||
success: true,
|
||||
emailId: result.id,
|
||||
message: 'Email sent successfully',
|
||||
status: 'sent'
|
||||
})
|
||||
} catch (processError) {
|
||||
// If immediate processing fails, return that it's queued
|
||||
console.warn('Failed to process email immediately, left in queue:', processError)
|
||||
return Response.json({
|
||||
success: true,
|
||||
emailId: result.id,
|
||||
message: 'Email queued successfully (immediate processing failed)',
|
||||
status: 'queued'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
emailId: result.id,
|
||||
message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully',
|
||||
status: scheduledAt ? 'scheduled' : 'queued'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Test email error:', error)
|
||||
|
||||
11
dev/app/page.tsx
Normal file
11
dev/app/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Metadata } from 'next'
|
||||
import {redirect} from "next/navigation.js"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'PayloadCMS Mailing Plugin - Development',
|
||||
description: 'Development environment for PayloadCMS Mailing Plugin',
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
redirect('/dashboard')
|
||||
}
|
||||
Reference in New Issue
Block a user