Add Mailgun admin tools and contact API

This commit is contained in:
2025-09-24 13:59:26 +00:00
parent 6329ad0618
commit 1054f5d817
13 changed files with 1459 additions and 29 deletions

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { useSearchParams } from 'next/navigation'
import { useSearchParams, useRouter } from 'next/navigation'
import {
Box,
Typography,
@@ -126,6 +126,7 @@ export default function BibleReaderNew() {
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const t = useTranslations('pages.bible')
const locale = useLocale()
const router = useRouter()
const searchParams = useSearchParams()
const { user } = useAuth()
@@ -261,6 +262,68 @@ export default function BibleReaderNew() {
})
}, [locale, showAllVersions, selectedVersion])
// Handle URL parameters for bookmark navigation
useEffect(() => {
const urlVersion = searchParams.get('version')
const urlBook = searchParams.get('book')
const urlChapter = searchParams.get('chapter')
const urlVerse = searchParams.get('verse')
if (urlVersion && versions.length > 0) {
// Check if this version exists
const version = versions.find(v => v.id === urlVersion)
if (version && selectedVersion !== urlVersion) {
setSelectedVersion(urlVersion)
}
}
if (urlBook && books.length > 0) {
const book = books.find(b => b.id === urlBook)
if (book && selectedBook !== urlBook) {
setSelectedBook(urlBook)
}
}
if (urlChapter) {
const chapter = parseInt(urlChapter)
if (chapter && selectedChapter !== chapter) {
setSelectedChapter(chapter)
}
}
if (urlVerse && verses.length > 0) {
const verseNum = parseInt(urlVerse)
if (verseNum) {
// Highlight the verse
setTimeout(() => {
const verseElement = verseRefs.current[verseNum]
if (verseElement) {
verseElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
setHighlightedVerse(verseNum)
}
}, 500)
}
}
}, [searchParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter])
// Function to update URL without causing full page reload
const updateUrl = useCallback((bookId?: string, chapter?: number, versionId?: string) => {
const params = new URLSearchParams()
if (versionId || selectedVersion) {
params.set('version', versionId || selectedVersion)
}
if (bookId || selectedBook) {
params.set('book', bookId || selectedBook)
}
if (chapter || selectedChapter) {
params.set('chapter', String(chapter || selectedChapter))
}
const newUrl = `/${locale}/bible?${params.toString()}`
router.replace(newUrl, { scroll: false })
}, [locale, selectedVersion, selectedBook, selectedChapter, router])
// Fetch books when version changes
useEffect(() => {
if (selectedVersion) {
@@ -431,16 +494,6 @@ export default function BibleReaderNew() {
const currentBook = books.find(book => book.id === selectedBook)
const maxChapters = currentBook?.chapters?.length || 1
const updateUrl = (bookId: string, chapter: number, version?: string) => {
const url = new URL(window.location.href)
url.searchParams.set('book', bookId)
url.searchParams.set('chapter', chapter.toString())
if (version) {
url.searchParams.set('version', version)
}
window.history.replaceState({}, '', url.toString())
}
const scrollToVerse = (verseNum: number) => {
const verseElement = verseRefs.current[verseNum]
if (verseElement) {

View File

@@ -50,6 +50,7 @@ interface BookmarkItem {
bookId: string
chapterNum: number
verseNum?: number
versionId: string
}
verse?: {
id: string
@@ -118,7 +119,8 @@ export default function BookmarksPage() {
const handleNavigateToBookmark = (bookmark: BookmarkItem) => {
const params = new URLSearchParams({
book: bookmark.navigation.bookId,
chapter: bookmark.navigation.chapterNum.toString()
chapter: bookmark.navigation.chapterNum.toString(),
version: bookmark.navigation.versionId
})
if (bookmark.navigation.verseNum) {

View File

@@ -50,20 +50,30 @@ export default function Contact() {
setIsSubmitting(true)
try {
// Simulate form submission
await new Promise(resolve => setTimeout(resolve, 1000))
// Here you would typically send the data to your API
console.log('Form submitted:', formData)
setFormData({
name: '',
email: '',
subject: '',
message: ''
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
setShowSuccess(true)
const data = await response.json()
if (response.ok && data.success) {
setFormData({
name: '',
email: '',
subject: '',
message: ''
})
setShowSuccess(true)
} else {
console.error('Contact form error:', data.error)
setShowError(true)
}
} catch (error) {
console.error('Contact form submission error:', error)
setShowError(true)
} finally {
setIsSubmitting(false)

437
app/admin/mailgun/page.tsx Normal file
View File

@@ -0,0 +1,437 @@
'use client'
import { useState, useEffect } from 'react'
import {
Box,
Paper,
Typography,
TextField,
Button,
FormControl,
InputLabel,
Select,
MenuItem,
Switch,
FormControlLabel,
Alert,
Divider,
Card,
CardContent,
InputAdornment,
IconButton,
Chip
} from '@mui/material'
import {
Save as SaveIcon,
PlayArrow as TestIcon,
Visibility,
VisibilityOff,
Email as EmailIcon,
Settings as SettingsIcon,
CheckCircle,
Error as ErrorIcon
} from '@mui/icons-material'
interface MailgunSettings {
id?: string
apiKey?: string
domain: string
region: string
fromEmail: string
fromName: string
replyToEmail?: string
isEnabled: boolean
testMode: boolean
webhookUrl?: string
createdAt?: string
updatedAt?: string
}
export default function MailgunSettingsPage() {
const [settings, setSettings] = useState<MailgunSettings>({
domain: '',
region: 'US',
fromEmail: '',
fromName: '',
isEnabled: false,
testMode: true
})
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [testEmail, setTestEmail] = useState('')
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'connected' | 'error'>('unknown')
useEffect(() => {
loadSettings()
}, [])
const loadSettings = async () => {
setLoading(true)
try {
const response = await fetch('/api/admin/mailgun', {
credentials: 'include'
})
if (response.ok) {
const data = await response.json()
if (data.success && data.data) {
setSettings(data.data)
if (data.data.isEnabled) {
testConnection()
}
}
}
} catch (error) {
console.error('Error loading settings:', error)
setMessage({ type: 'error', text: 'Failed to load settings' })
} finally {
setLoading(false)
}
}
const handleSave = async () => {
setSaving(true)
setMessage(null)
try {
const method = settings.id ? 'PUT' : 'POST'
const response = await fetch('/api/admin/mailgun', {
method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(settings)
})
const data = await response.json()
if (response.ok && data.success) {
setSettings(data.data)
setMessage({ type: 'success', text: 'Settings saved successfully' })
if (data.data.isEnabled) {
testConnection()
}
} else {
setMessage({ type: 'error', text: data.error || 'Failed to save settings' })
}
} catch (error) {
console.error('Error saving settings:', error)
setMessage({ type: 'error', text: 'Failed to save settings' })
} finally {
setSaving(false)
}
}
const testConnection = async () => {
setTesting(true)
try {
const response = await fetch('/api/admin/mailgun/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ testType: 'connection' })
})
const data = await response.json()
if (data.success) {
setConnectionStatus('connected')
setMessage({ type: 'success', text: 'Connection test successful' })
} else {
setConnectionStatus('error')
setMessage({ type: 'error', text: `Connection failed: ${data.error}` })
}
} catch (error) {
setConnectionStatus('error')
setMessage({ type: 'error', text: 'Connection test failed' })
} finally {
setTesting(false)
}
}
const sendTestEmail = async () => {
if (!testEmail) {
setMessage({ type: 'error', text: 'Please enter a test email address' })
return
}
setTesting(true)
try {
const response = await fetch('/api/admin/mailgun/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ testType: 'email', email: testEmail })
})
const data = await response.json()
if (data.success) {
setMessage({ type: 'success', text: `Test email sent successfully! ${data.messageId ? `Message ID: ${data.messageId}` : ''}` })
} else {
setMessage({ type: 'error', text: `Failed to send test email: ${data.error}` })
}
} catch (error) {
setMessage({ type: 'error', text: 'Failed to send test email' })
} finally {
setTesting(false)
}
}
const handleChange = (field: keyof MailgunSettings, value: any) => {
setSettings(prev => ({ ...prev, [field]: value }))
}
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<EmailIcon sx={{ mr: 2, color: 'primary.main' }} />
<Typography variant="h4" component="h1">
Mailgun Settings
</Typography>
</Box>
{message && (
<Alert
severity={message.type}
sx={{ mb: 3 }}
onClose={() => setMessage(null)}
>
{message.text}
</Alert>
)}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Connection Status */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<SettingsIcon sx={{ mr: 2 }} />
<Typography variant="h6">Connection Status</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{connectionStatus === 'connected' && (
<Chip
label="Connected"
color="success"
icon={<CheckCircle />}
size="small"
/>
)}
{connectionStatus === 'error' && (
<Chip
label="Connection Error"
color="error"
icon={<ErrorIcon />}
size="small"
/>
)}
{connectionStatus === 'unknown' && (
<Chip
label="Unknown"
color="default"
size="small"
/>
)}
<Button
variant="outlined"
size="small"
onClick={testConnection}
disabled={testing || !settings.isEnabled}
startIcon={<TestIcon />}
>
Test Connection
</Button>
</Box>
</Box>
</CardContent>
</Card>
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', md: 'row' } }}>
{/* Main Settings */}
<Paper sx={{ p: 3, flex: { xs: '1 1 100%', md: '2 1 0' } }}>
<Typography variant="h6" sx={{ mb: 3 }}>
Mailgun Configuration
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<TextField
label="API Key"
type={showApiKey ? 'text' : 'password'}
value={settings.apiKey || ''}
onChange={(e) => handleChange('apiKey', e.target.value)}
placeholder="key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowApiKey(!showApiKey)}
edge="end"
>
{showApiKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
helperText="Your Mailgun API key"
/>
<TextField
label="Domain"
value={settings.domain}
onChange={(e) => handleChange('domain', e.target.value)}
placeholder="mg.yourdomain.com"
fullWidth
helperText="Your Mailgun domain"
/>
<FormControl fullWidth>
<InputLabel>Region</InputLabel>
<Select
value={settings.region}
onChange={(e) => handleChange('region', e.target.value)}
label="Region"
>
<MenuItem value="US">US</MenuItem>
<MenuItem value="EU">EU</MenuItem>
</Select>
</FormControl>
<Divider />
<Typography variant="h6">Email Settings</Typography>
<TextField
label="From Email"
type="email"
value={settings.fromEmail}
onChange={(e) => handleChange('fromEmail', e.target.value)}
placeholder="noreply@yourdomain.com"
fullWidth
helperText="Default sender email address"
/>
<TextField
label="From Name"
value={settings.fromName}
onChange={(e) => handleChange('fromName', e.target.value)}
placeholder="Biblical Guide"
fullWidth
helperText="Default sender name"
/>
<TextField
label="Reply-To Email (Optional)"
type="email"
value={settings.replyToEmail || ''}
onChange={(e) => handleChange('replyToEmail', e.target.value)}
placeholder="support@yourdomain.com"
fullWidth
helperText="Default reply-to address"
/>
<TextField
label="Webhook URL (Optional)"
type="url"
value={settings.webhookUrl || ''}
onChange={(e) => handleChange('webhookUrl', e.target.value)}
placeholder="https://yourdomain.com/api/webhooks/mailgun"
fullWidth
helperText="URL for Mailgun webhooks (tracking, bounces, etc.)"
/>
<Divider />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.isEnabled}
onChange={(e) => handleChange('isEnabled', e.target.checked)}
/>
}
label="Enable Mailgun"
/>
<FormControlLabel
control={
<Switch
checked={settings.testMode}
onChange={(e) => handleChange('testMode', e.target.checked)}
/>
}
label="Test Mode (emails won't actually be sent)"
/>
</Box>
<Button
variant="contained"
onClick={handleSave}
disabled={saving || loading}
startIcon={<SaveIcon />}
size="large"
sx={{ alignSelf: 'flex-start' }}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</Box>
</Paper>
{/* Test Email */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, flex: { xs: '1 1 100%', md: '1 1 0' } }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" sx={{ mb: 3 }}>
Test Email
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Test Email Address"
type="email"
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
placeholder="test@example.com"
fullWidth
/>
<Button
variant="outlined"
onClick={sendTestEmail}
disabled={testing || !settings.isEnabled || !testEmail}
startIcon={<EmailIcon />}
fullWidth
>
{testing ? 'Sending...' : 'Send Test Email'}
</Button>
</Box>
{settings.testMode && (
<Alert severity="info" sx={{ mt: 2 }}>
Test mode is enabled. Emails will be logged but not actually sent.
</Alert>
)}
</Paper>
{settings.updatedAt && (
<Paper sx={{ p: 2 }}>
<Typography variant="body2" color="text.secondary">
Last updated: {new Date(settings.updatedAt).toLocaleString()}
</Typography>
</Paper>
)}
</Box>
</Box>
</Box>
</Box>
)
}

View File

@@ -0,0 +1,231 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { verifyAdminAuth } from '@/lib/admin-auth'
import { mailgunService } from '@/lib/mailgun'
export const runtime = 'nodejs'
export async function GET(request: NextRequest) {
try {
const adminUser = await verifyAdminAuth(request)
if (!adminUser) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const settings = await prisma.mailgunSettings.findFirst({
orderBy: { updatedAt: 'desc' },
select: {
id: true,
domain: true,
region: true,
fromEmail: true,
fromName: true,
replyToEmail: true,
isEnabled: true,
testMode: true,
webhookUrl: true,
createdAt: true,
updatedAt: true,
// Don't return the API key for security
apiKey: false
}
})
return NextResponse.json({
success: true,
data: settings
})
} catch (error) {
console.error('Error fetching Mailgun settings:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const adminUser = await verifyAdminAuth(request)
if (!adminUser) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const {
apiKey,
domain,
region = 'US',
fromEmail,
fromName,
replyToEmail,
isEnabled = false,
testMode = true,
webhookUrl
} = body
// Validate required fields
if (!apiKey || !domain || !fromEmail || !fromName) {
return NextResponse.json({
error: 'API key, domain, from email, and from name are required'
}, { status: 400 })
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(fromEmail)) {
return NextResponse.json({
error: 'Invalid from email format'
}, { status: 400 })
}
if (replyToEmail && !emailRegex.test(replyToEmail)) {
return NextResponse.json({
error: 'Invalid reply-to email format'
}, { status: 400 })
}
// Disable existing settings
await prisma.mailgunSettings.updateMany({
data: { isEnabled: false }
})
// Create new settings
const settings = await prisma.mailgunSettings.create({
data: {
apiKey,
domain,
region,
fromEmail,
fromName,
replyToEmail,
isEnabled,
testMode,
webhookUrl,
updatedBy: adminUser.id
}
})
// Clear service cache
mailgunService.clearCache()
return NextResponse.json({
success: true,
data: {
id: settings.id,
domain: settings.domain,
region: settings.region,
fromEmail: settings.fromEmail,
fromName: settings.fromName,
replyToEmail: settings.replyToEmail,
isEnabled: settings.isEnabled,
testMode: settings.testMode,
webhookUrl: settings.webhookUrl,
createdAt: settings.createdAt,
updatedAt: settings.updatedAt
}
})
} catch (error) {
console.error('Error creating Mailgun settings:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
export async function PUT(request: NextRequest) {
try {
const adminUser = await verifyAdminAuth(request)
if (!adminUser) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const {
id,
apiKey,
domain,
region,
fromEmail,
fromName,
replyToEmail,
isEnabled,
testMode,
webhookUrl
} = body
if (!id) {
return NextResponse.json({ error: 'Settings ID is required' }, { status: 400 })
}
// Validate required fields
if (!domain || !fromEmail || !fromName) {
return NextResponse.json({
error: 'Domain, from email, and from name are required'
}, { status: 400 })
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(fromEmail)) {
return NextResponse.json({
error: 'Invalid from email format'
}, { status: 400 })
}
if (replyToEmail && !emailRegex.test(replyToEmail)) {
return NextResponse.json({
error: 'Invalid reply-to email format'
}, { status: 400 })
}
// If enabling this setting, disable others
if (isEnabled) {
await prisma.mailgunSettings.updateMany({
where: { id: { not: id } },
data: { isEnabled: false }
})
}
// Build update data
const updateData: any = {
domain,
region,
fromEmail,
fromName,
replyToEmail,
isEnabled,
testMode,
webhookUrl,
updatedBy: adminUser.id
}
// Only update API key if provided
if (apiKey) {
updateData.apiKey = apiKey
}
const settings = await prisma.mailgunSettings.update({
where: { id },
data: updateData
})
// Clear service cache
mailgunService.clearCache()
return NextResponse.json({
success: true,
data: {
id: settings.id,
domain: settings.domain,
region: settings.region,
fromEmail: settings.fromEmail,
fromName: settings.fromName,
replyToEmail: settings.replyToEmail,
isEnabled: settings.isEnabled,
testMode: settings.testMode,
webhookUrl: settings.webhookUrl,
createdAt: settings.createdAt,
updatedAt: settings.updatedAt
}
})
} catch (error) {
console.error('Error updating Mailgun settings:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server'
import { verifyAdminAuth } from '@/lib/admin-auth'
import { mailgunService } from '@/lib/mailgun'
export const runtime = 'nodejs'
export async function POST(request: NextRequest) {
try {
const adminUser = await verifyAdminAuth(request)
if (!adminUser) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { testType, email } = body
if (testType === 'connection') {
// Test connection to Mailgun
const result = await mailgunService.testConnection()
return NextResponse.json(result)
}
if (testType === 'email') {
// Send test email
if (!email) {
return NextResponse.json({
success: false,
error: 'Email address is required for email test'
}, { status: 400 })
}
const result = await mailgunService.sendEmail({
to: email,
subject: 'Biblical Guide - Email Configuration Test',
text: `Hello,
This is a configuration test email from Biblical Guide.
Your email system has been successfully configured and is working properly. You can now receive important notifications from our platform.
Best regards,
The Biblical Guide Team
---
This is an automated message. Please do not reply to this email.
Time: ${new Date().toLocaleString()}`,
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Biblical Guide - Email Configuration Test</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h2 style="color: #2c3e50; margin: 0;">Biblical Guide</h2>
<p style="margin: 5px 0 0 0; color: #6c757d;">Email Configuration Test</p>
</div>
<div style="background-color: white; padding: 20px; border-radius: 8px; border: 1px solid #e9ecef;">
<p>Hello,</p>
<p>This is a configuration test email from Biblical Guide.</p>
<p><strong>Your email system has been successfully configured and is working properly.</strong> You can now receive important notifications from our platform.</p>
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; padding: 15px; margin: 20px 0;">
<p style="margin: 0; color: #155724;"><strong>✓ Configuration successful</strong></p>
</div>
<p>Best regards,<br>
The Biblical Guide Team</p>
</div>
<div style="margin-top: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; font-size: 12px; color: #6c757d;">
<p style="margin: 0;">This is an automated message. Please do not reply to this email.</p>
<p style="margin: 5px 0 0 0;">Time: ${new Date().toLocaleString()}</p>
</div>
</body>
</html>
`
})
return NextResponse.json(result)
}
return NextResponse.json({
success: false,
error: 'Invalid test type'
}, { status: 400 })
} catch (error) {
console.error('Error testing Mailgun:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Test failed'
}, { status: 500 })
}
}

View File

@@ -86,12 +86,13 @@ export async function GET(request: Request) {
id: bookmark.id,
type: 'chapter' as const,
title: `${bookmark.book.name} ${bookmark.chapterNum}`,
subtitle: bookmark.book.version.name,
subtitle: `${bookmark.book.version.name} - ${bookmark.book.version.abbreviation}`,
note: bookmark.note,
createdAt: bookmark.createdAt,
navigation: {
bookId: bookmark.bookId,
chapterNum: bookmark.chapterNum
chapterNum: bookmark.chapterNum,
versionId: bookmark.book.versionId
},
book: bookmark.book
}))
@@ -100,7 +101,7 @@ export async function GET(request: Request) {
id: bookmark.id,
type: 'verse' as const,
title: `${bookmark.verse.chapter.book.name} ${bookmark.verse.chapter.chapterNum}:${bookmark.verse.verseNum}`,
subtitle: bookmark.verse.chapter.book.version.name,
subtitle: `${bookmark.verse.chapter.book.version.name} - ${bookmark.verse.chapter.book.version.abbreviation}`,
note: bookmark.note,
createdAt: bookmark.createdAt,
color: bookmark.color,
@@ -108,7 +109,8 @@ export async function GET(request: Request) {
navigation: {
bookId: bookmark.verse.chapter.bookId,
chapterNum: bookmark.verse.chapter.chapterNum,
verseNum: bookmark.verse.verseNum
verseNum: bookmark.verse.verseNum,
versionId: bookmark.verse.chapter.book.versionId
},
verse: bookmark.verse
}))

77
app/api/contact/route.ts Normal file
View File

@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from 'next/server'
import { mailgunService } from '@/lib/mailgun'
import { z } from 'zod'
export const runtime = 'nodejs'
const contactSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email address'),
subject: z.string().min(1, 'Subject is required').max(200),
message: z.string().min(10, 'Message must be at least 10 characters').max(5000)
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate input
const validationResult = contactSchema.safeParse(body)
if (!validationResult.success) {
return NextResponse.json({
success: false,
error: 'Invalid form data',
details: validationResult.error.errors
}, { status: 400 })
}
const { name, email, subject, message } = validationResult.data
// Basic spam prevention - check for common spam indicators
const spamIndicators = [
message.includes('http://'),
message.includes('https://'),
message.includes('www.'),
message.includes('bitcoin'),
message.includes('cryptocurrency'),
message.length < 10,
name.length < 2
]
const spamScore = spamIndicators.filter(Boolean).length
if (spamScore >= 2) {
return NextResponse.json({
success: false,
error: 'Message flagged as potential spam'
}, { status: 400 })
}
// Send email using Mailgun
const emailResult = await mailgunService.sendContactForm({
name,
email,
subject,
message
})
if (emailResult.success) {
return NextResponse.json({
success: true,
message: 'Your message has been sent successfully!'
})
} else {
console.error('Contact form email failed:', emailResult.error)
return NextResponse.json({
success: false,
error: 'Failed to send message. Please try again later.'
}, { status: 500 })
}
} catch (error) {
console.error('Contact form error:', error)
return NextResponse.json({
success: false,
error: 'Internal server error'
}, { status: 500 })
}
}