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:
2025-09-14 19:48:45 +02:00
parent 0d295603ef
commit 8993d20526
16 changed files with 496 additions and 593 deletions

View 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>
)
}

View 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>
)
}

View File

@@ -0,0 +1,384 @@
'use client'
import { useState, useEffect } from 'react'
interface Template {
id: string
name: string
slug: string
subject: string
variables?: Array<{
name: string
type: string
required: boolean
description?: string
}>
previewData?: Record<string, any>
}
interface QueuedEmail {
id: string
subject: string
to: string[]
status: string
createdAt: string
sentAt?: string
attempts: number
error?: string
}
export default function MailingTestPage() {
const [templates, setTemplates] = useState<Template[]>([])
const [queuedEmails, setQueuedEmails] = useState<QueuedEmail[]>([])
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)
const [message, setMessage] = useState<string>('')
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
try {
const response = await fetch('/api/test-email')
const data = await response.json()
setTemplates(data.templates || [])
setQueuedEmails(data.outbox?.emails || [])
} catch (error) {
console.error('Error fetching data:', error)
}
}
const handleTemplateChange = (templateSlug: string) => {
setSelectedTemplate(templateSlug)
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')
}
}
const sendTestEmail = async () => {
if (!selectedTemplate || !toEmail) {
setMessage('Please select a template and enter an email address')
return
}
if (jsonError) {
setMessage('Please fix the JSON syntax error before sending')
return
}
setLoading(true)
setMessage('')
try {
const response = await fetch('/api/test-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: emailType,
templateSlug: selectedTemplate,
to: toEmail,
variables,
scheduledAt: emailType === 'schedule' ? scheduleDate : undefined,
}),
})
const result = await response.json()
if (result.success) {
const statusIcon = result.status === 'sent' ? '📧' : '📫'
setMessage(`${statusIcon} ${result.message} (ID: ${result.emailId})`)
fetchData() // Refresh email queue
} else {
setMessage(`❌ Error: ${result.error}`)
}
} catch (error) {
setMessage(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setLoading(false)
}
}
const processEmailQueue = async () => {
setLoading(true)
try {
const response = await fetch('/api/process-emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const result = await response.json()
setMessage(result.success ? `${result.message}` : `${result.error}`)
setTimeout(() => {
fetchData() // Refresh after a delay to see status changes
}, 1000)
} catch (error) {
setMessage(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setLoading(false)
}
}
const selectedTemplateData = templates.find(t => t.slug === selectedTemplate)
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '1200px', margin: '0 auto' }}>
<h1>📧 PayloadCMS Mailing Plugin Test</h1>
{message && (
<div style={{
padding: '10px',
margin: '10px 0',
backgroundColor: message.startsWith('✅') ? '#d4edda' : '#f8d7da',
border: `1px solid ${message.startsWith('✅') ? '#c3e6cb' : '#f5c6cb'}`,
borderRadius: '4px',
}}>
{message}
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
{/* Send Email Form */}
<div style={{ border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
<h2>Send Test Email</h2>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Email Template:</label>
<select
value={selectedTemplate}
onChange={(e) => handleTemplateChange(e.target.value)}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
>
<option value="">Select a template...</option>
{templates.map(template => (
<option key={template.id} value={template.slug}>{template.name}</option>
))}
</select>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>To Email:</label>
<input
type="email"
value={toEmail}
onChange={(e) => setToEmail(e.target.value)}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Type:</label>
<label style={{ marginRight: '15px' }}>
<input
type="radio"
value="send"
checked={emailType === 'send'}
onChange={(e) => setEmailType(e.target.value as 'send' | 'schedule')}
/>
Send Now
</label>
<label>
<input
type="radio"
value="schedule"
checked={emailType === 'schedule'}
onChange={(e) => setEmailType(e.target.value as 'send' | 'schedule')}
/>
Schedule
</label>
</div>
{emailType === 'schedule' && (
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Schedule Date:</label>
<input
type="datetime-local"
value={scheduleDate}
onChange={(e) => setScheduleDate(e.target.value)}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
)}
{selectedTemplate && (
<div style={{ marginBottom: '15px' }}>
<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>
)}
<button
onClick={sendTestEmail}
disabled={loading || !selectedTemplate || !toEmail}
style={{
width: '100%',
padding: '12px',
backgroundColor: loading ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
marginBottom: '10px',
}}
>
{loading ? 'Sending...' : emailType === 'send' ? 'Send Email' : 'Schedule Email'}
</button>
<button
onClick={processEmailQueue}
disabled={loading}
style={{
width: '100%',
padding: '12px',
backgroundColor: loading ? '#ccc' : '#17a2b8',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
marginTop: '10px',
}}
>
{loading ? 'Queuing...' : 'Process Email Queue (Pending + Failed)'}
</button>
</div>
{/* Email Queue */}
<div style={{ border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h2>Email Queue</h2>
<button
onClick={fetchData}
style={{
padding: '8px 16px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Refresh
</button>
</div>
{queuedEmails.length === 0 ? (
<p style={{ color: '#666' }}>No emails in queue</p>
) : (
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
{queuedEmails.map(email => (
<div
key={email.id}
style={{
border: '1px solid #eee',
padding: '12px',
marginBottom: '10px',
borderRadius: '4px',
backgroundColor: email.status === 'sent' ? '#f8f9fa' :
email.status === 'failed' ? '#fff3cd' :
email.status === 'processing' ? '#e3f2fd' : 'white',
}}
>
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>{email.subject}</div>
<div style={{ fontSize: '14px', color: '#666' }}>
To: {Array.isArray(email.to) ? email.to.join(', ') : email.to} | Status: <strong>{email.status}</strong> | Attempts: {email.attempts}
</div>
<div style={{ fontSize: '12px', color: '#999' }}>
Created: {new Date(email.createdAt).toLocaleString()}
{email.sentAt && ` | Sent: ${new Date(email.sentAt).toLocaleString()}`}
</div>
{email.error && (
<div style={{ fontSize: '12px', color: '#dc3545', marginTop: '5px' }}>
Error: {email.error}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* Templates Overview */}
<div style={{ marginTop: '30px', border: '1px solid #ddd', padding: '20px', borderRadius: '8px' }}>
<h2>Available Templates</h2>
{templates.length === 0 ? (
<p style={{ color: '#666' }}>No templates available</p>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '15px' }}>
{templates.map(template => (
<div key={template.id} style={{ border: '1px solid #eee', padding: '15px', borderRadius: '4px' }}>
<h3 style={{ margin: '0 0 10px 0' }}>{template.name}</h3>
<p style={{ margin: '0 0 10px 0', color: '#666', fontSize: '14px' }}>{template.subject}</p>
<div style={{ fontSize: '12px', color: '#999' }}>
Variables: {template.variables?.length || 0}
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}