feat: Add prayer chains, social sharing, and testimony wall
- Add PrayerChain component for visualizing linked prayers - Add SharePrayer component with social media integration (Twitter, Facebook, WhatsApp, copy link) - Create /testimonies page for public answered prayer stories - Update Firestore rules for public testimony access - Enhance Organization tab with prayer chains - Add Testimonies navigation tab - Fix Firebase Admin SDK integration for server-side operations - Improve email system with proper server-side authentication - Update prayer form to handle optional fields correctly - Deploy updated Firestore security rules
This commit is contained in:
1 parent
7214fdb89a
commit
d9784738e4
32 files changed
+3146
-3236
No files matched your search
@@ -1,43 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { doc, getDoc, collection, getDocs, updateDoc } from 'firebase/firestore';
|
||||
import { db } from '@/lib/firebase';
|
||||
import { admin } from '@/lib/firebase-admin';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
function generatePrayerEmailHTML(message: string, subject: string) {
|
||||
// Parse the message to extract prayer details
|
||||
// Parse the message to extract prayer details and stats
|
||||
const lines = message.split('\n');
|
||||
let currentPrayer = null;
|
||||
const prayers = [];
|
||||
let summarySection = false;
|
||||
const stats = { total: 0, active: 0, answered: 0, archived: 0 };
|
||||
const activePrayers = [];
|
||||
const answeredPrayers = [];
|
||||
let inActiveSection = false;
|
||||
let inAnsweredSection = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('New prayer added') || line.includes('New praise added')) {
|
||||
currentPrayer = { type: '', date: '', text: '', journal: '', prayFor: '' };
|
||||
} else if (line.startsWith('Date: ')) {
|
||||
if (currentPrayer) currentPrayer.date = line.replace('Date: ', '');
|
||||
} else if (line.startsWith('Praying for: ')) {
|
||||
if (currentPrayer) currentPrayer.prayFor = line.replace('Praying for: ', '');
|
||||
} else if (line.startsWith('Journal: ')) {
|
||||
if (currentPrayer) currentPrayer.journal = line.replace('Journal: ', '');
|
||||
} else if (line.includes('--- Active Prayers Summary ---')) {
|
||||
summarySection = true;
|
||||
prayers.push({ type: 'summary-header', text: 'Active Prayers Summary' });
|
||||
} else if (summarySection && line.includes(' - ')) {
|
||||
// Parse summary lines like "2025-01-15 - PRAYER: prayer text"
|
||||
if (line.includes('Total Prayers:')) {
|
||||
stats.total = parseInt(line.split(':')[1].trim()) || 0;
|
||||
} else if (line.includes('Active Prayers:')) {
|
||||
stats.active = parseInt(line.split(':')[1].trim()) || 0;
|
||||
} else if (line.includes('Answered Prayers:')) {
|
||||
stats.answered = parseInt(line.split(':')[1].trim()) || 0;
|
||||
} else if (line.includes('Archived Prayers:')) {
|
||||
stats.archived = parseInt(line.split(':')[1].trim()) || 0;
|
||||
} else if (line.includes('--- Your Active Prayers ---')) {
|
||||
inActiveSection = true;
|
||||
inAnsweredSection = false;
|
||||
} else if (line.includes('--- Recently Answered Prayers ---')) {
|
||||
inActiveSection = false;
|
||||
inAnsweredSection = true;
|
||||
} else if (inActiveSection && line.includes(' - ')) {
|
||||
const match = line.match(/(\d{4}-\d{2}-\d{2}) - (PRAYER|PRAISE): (.+)/);
|
||||
if (match) {
|
||||
prayers.push({
|
||||
type: 'summary-item',
|
||||
activePrayers.push({
|
||||
date: match[1],
|
||||
prayerType: match[2].toLowerCase(),
|
||||
type: match[2].toLowerCase(),
|
||||
text: match[3]
|
||||
});
|
||||
}
|
||||
} else if (inAnsweredSection && line.includes(' - ')) {
|
||||
const match = line.match(/(\d{4}-\d{2}-\d{2}) - (PRAYER|PRAISE): (.+)/);
|
||||
if (match) {
|
||||
answeredPrayers.push({
|
||||
date: match[1],
|
||||
type: match[2].toLowerCase(),
|
||||
text: match[3]
|
||||
});
|
||||
}
|
||||
} else if (currentPrayer && !currentPrayer.text && line.trim()) {
|
||||
currentPrayer.text = line;
|
||||
prayers.push(currentPrayer);
|
||||
currentPrayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,10 +177,48 @@ function generatePrayerEmailHTML(message: string, subject: string) {
|
||||
font-size: 16px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.verse cite {
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
margin: 25px 0;
|
||||
}
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.section {
|
||||
margin: 30px 0;
|
||||
}
|
||||
.section h3 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.celebration {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
border-radius: 12px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.celebration h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -185,34 +229,57 @@ function generatePrayerEmailHTML(message: string, subject: string) {
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${prayers.map(prayer => {
|
||||
if (prayer.type === 'summary-header') {
|
||||
return `
|
||||
<div class="summary-section">
|
||||
<h3>📚 ${prayer.text}</h3>
|
||||
<p>Here are all your active prayers and praises:</p>
|
||||
</div>
|
||||
`;
|
||||
} else if (prayer.type === 'summary-item') {
|
||||
return `
|
||||
<div class="summary-item">
|
||||
<div class="date">${prayer.date ? new Date(prayer.date).toLocaleDateString() : 'Unknown Date'}</div>
|
||||
<div class="prayer-type">${prayer.prayerType}</div>
|
||||
<div class="prayer-text">${prayer.text}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="prayer-card">
|
||||
<div class="prayer-type">${prayer.type}</div>
|
||||
<div class="prayer-date">${prayer.date ? new Date(prayer.date).toLocaleDateString() : 'Unknown Date'}</div>
|
||||
<div class="prayer-text">${prayer.text}</div>
|
||||
${prayer.prayFor ? `<div class="pray-for">🙋 Praying for: ${prayer.prayFor}</div>` : ''}
|
||||
${prayer.journal && prayer.journal !== 'None' ? `<div class="prayer-journal">📝 Journal: ${prayer.journal}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('')}
|
||||
<!-- Prayer Statistics -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">${stats.total}</div>
|
||||
<div class="stat-label">Total Prayers</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">${stats.active}</div>
|
||||
<div class="stat-label">Active</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">${stats.answered}</div>
|
||||
<div class="stat-label">Answered</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">${stats.archived}</div>
|
||||
<div class="stat-label">Archived</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${activePrayers.length > 0 ? `
|
||||
<div class="section">
|
||||
<h3>📝 Your Active Prayers</h3>
|
||||
${activePrayers.map(prayer => `
|
||||
<div class="prayer-card">
|
||||
<div class="prayer-type">${prayer.type}</div>
|
||||
<div class="prayer-date">${new Date(prayer.date).toLocaleDateString()}</div>
|
||||
<div class="prayer-text">${prayer.text}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div class="celebration">
|
||||
<h3>🎉 All Caught Up!</h3>
|
||||
<p>You have no active prayers. Great job staying on top of your prayer life!</p>
|
||||
</div>
|
||||
`}
|
||||
|
||||
${answeredPrayers.length > 0 ? `
|
||||
<div class="section">
|
||||
<h3>🙏 Recently Answered Prayers</h3>
|
||||
${answeredPrayers.map(prayer => `
|
||||
<div class="prayer-card answered">
|
||||
<div class="prayer-type">${prayer.type}</div>
|
||||
<div class="prayer-date">${new Date(prayer.date).toLocaleDateString()}</div>
|
||||
<div class="prayer-text">${prayer.text}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="verse">
|
||||
<blockquote>"Pray without ceasing"</blockquote>
|
||||
@@ -234,9 +301,9 @@ export async function GET() {
|
||||
try {
|
||||
console.log('🔄 Checking for scheduled emails...');
|
||||
|
||||
// Get SMTP config
|
||||
const configDoc = await getDoc(doc(db, 'config', 'smtp'));
|
||||
if (!configDoc.exists()) {
|
||||
// Get SMTP config using Firebase Admin SDK
|
||||
const configDoc = await admin.firestore().collection('config').doc('smtp').get();
|
||||
if (!configDoc.exists) {
|
||||
console.log('❌ SMTP configuration not found');
|
||||
return NextResponse.json({ message: 'SMTP configuration not found' }, { status: 500 });
|
||||
}
|
||||
@@ -254,9 +321,8 @@ export async function GET() {
|
||||
},
|
||||
});
|
||||
|
||||
// Get all users who have reminder preferences
|
||||
const usersRef = collection(db, 'users');
|
||||
const usersSnapshot = await getDocs(usersRef);
|
||||
// Get all users (send daily emails to everyone) using Firebase Admin SDK
|
||||
const usersSnapshot = await admin.firestore().collection('users').get();
|
||||
|
||||
let emailsSent = 0;
|
||||
|
||||
@@ -264,58 +330,78 @@ export async function GET() {
|
||||
const userData = userDoc.data();
|
||||
const userId = userDoc.id;
|
||||
|
||||
// Check if user has reminder preferences
|
||||
if (!userData.reminderFrequency || userData.reminderFrequency === 'never') {
|
||||
// Skip users without email
|
||||
if (!userData.email) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check last email sent time
|
||||
// Check last email sent time - send daily
|
||||
const lastEmailSent = userData.lastEmailSent ? new Date(userData.lastEmailSent.toDate()) : new Date(0);
|
||||
const now = new Date();
|
||||
|
||||
let shouldSendEmail = false;
|
||||
const frequency = userData.reminderFrequency;
|
||||
|
||||
if (frequency === 'daily') {
|
||||
// Send if more than 24 hours have passed
|
||||
shouldSendEmail = (now.getTime() - lastEmailSent.getTime()) > (24 * 60 * 60 * 1000);
|
||||
} else if (frequency === 'weekly') {
|
||||
// Send if more than 7 days have passed
|
||||
shouldSendEmail = (now.getTime() - lastEmailSent.getTime()) > (7 * 24 * 60 * 60 * 1000);
|
||||
} else if (frequency === 'monthly') {
|
||||
// Send if more than 30 days have passed
|
||||
shouldSendEmail = (now.getTime() - lastEmailSent.getTime()) > (30 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
// Send if more than 24 hours have passed
|
||||
const shouldSendEmail = (now.getTime() - lastEmailSent.getTime()) > (24 * 60 * 60 * 1000);
|
||||
|
||||
if (!shouldSendEmail) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get user's active prayers
|
||||
const prayersRef = collection(db, 'prayers');
|
||||
const prayersQuery = await getDocs(prayersRef);
|
||||
const userPrayers = prayersQuery.docs
|
||||
.filter(doc => doc.data().userId === userId && !doc.data().archived)
|
||||
.map(doc => doc.data());
|
||||
// Get user's prayers for stats using Firebase Admin SDK
|
||||
const prayersSnapshot = await admin.firestore().collection('prayers').where('userId', '==', userId).get();
|
||||
const userPrayers = prayersSnapshot.docs.map(doc => doc.data());
|
||||
|
||||
if (userPrayers.length === 0) {
|
||||
continue; // No active prayers to send
|
||||
// Calculate stats
|
||||
const activePrayers = userPrayers.filter(p => !p.archived);
|
||||
const archivedPrayers = userPrayers.filter(p => p.archived);
|
||||
const answeredPrayers = userPrayers.filter(p => p.answered);
|
||||
const totalPrayers = userPrayers.length;
|
||||
|
||||
// Build email content with stats
|
||||
let message = `--- Daily Prayer Summary ---\n\n`;
|
||||
message += `📊 Your Prayer Statistics:\n`;
|
||||
message += `• Total Prayers: ${totalPrayers}\n`;
|
||||
message += `• Active Prayers: ${activePrayers.length}\n`;
|
||||
message += `• Answered Prayers: ${answeredPrayers.length}\n`;
|
||||
message += `• Archived Prayers: ${archivedPrayers.length}\n\n`;
|
||||
|
||||
if (activePrayers.length > 0) {
|
||||
message += `--- Your Active Prayers ---\n`;
|
||||
activePrayers.forEach(prayer => {
|
||||
message += `\n${prayer.date} - ${prayer.type.toUpperCase()}: ${prayer.text}`;
|
||||
if (prayer.prayFor) {
|
||||
message += ` (Praying for: ${prayer.prayFor})`;
|
||||
}
|
||||
if (prayer.journal) {
|
||||
message += `\n Journal: ${prayer.journal}`;
|
||||
}
|
||||
message += `\n`;
|
||||
});
|
||||
} else {
|
||||
message += `🎉 Great job! You have no active prayers - all caught up!\n\n`;
|
||||
}
|
||||
|
||||
// Build email content
|
||||
let message = `--- Active Prayers Summary ---\n`;
|
||||
userPrayers.forEach(prayer => {
|
||||
message += `\n${prayer.date} - ${prayer.type.toUpperCase()}: ${prayer.text}`;
|
||||
if (prayer.prayFor) {
|
||||
message += ` (Praying for: ${prayer.prayFor})`;
|
||||
}
|
||||
if (prayer.journal) {
|
||||
message += `\n Journal: ${prayer.journal}`;
|
||||
}
|
||||
message += `\n`;
|
||||
});
|
||||
if (answeredPrayers.length > 0) {
|
||||
message += `\n--- Recently Answered Prayers ---\n`;
|
||||
// Show answered prayers from the last 7 days
|
||||
const recentAnswered = answeredPrayers.filter(p => {
|
||||
const answeredDate = p.answeredDate ? new Date(p.answeredDate) : new Date(p.date);
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
return answeredDate >= weekAgo;
|
||||
});
|
||||
|
||||
const subject = `Your ${frequency} prayer reminder - ${userPrayers.length} active prayers`;
|
||||
if (recentAnswered.length > 0) {
|
||||
recentAnswered.forEach(prayer => {
|
||||
message += `\n${prayer.date} - ${prayer.type.toUpperCase()}: ${prayer.text}`;
|
||||
if (prayer.answeredDate) {
|
||||
message += ` (Answered: ${new Date(prayer.answeredDate).toLocaleDateString()})`;
|
||||
}
|
||||
message += `\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const subject = `Daily Prayer Summary - ${activePrayers.length} active prayers`;
|
||||
|
||||
try {
|
||||
// Send email
|
||||
@@ -327,43 +413,40 @@ export async function GET() {
|
||||
html: generatePrayerEmailHTML(message, subject),
|
||||
});
|
||||
|
||||
// Send push notification if FCM token exists
|
||||
if (userData.fcmToken && admin.apps.length > 0) {
|
||||
try {
|
||||
await admin.messaging().send({
|
||||
token: userData.fcmToken,
|
||||
notification: {
|
||||
title: subject,
|
||||
body: `You have ${userPrayers.length} active prayers to review.`,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Sent push notification to ${userData.email}`);
|
||||
} catch (fcmError) {
|
||||
console.error(`❌ Failed to send FCM to ${userData.email}:`, fcmError);
|
||||
}
|
||||
}
|
||||
|
||||
// Update last email sent time
|
||||
await updateDoc(doc(db, 'users', userId), {
|
||||
// Update last email sent time using Firebase Admin SDK
|
||||
await admin.firestore().collection('users').doc(userId).update({
|
||||
lastEmailSent: new Date(),
|
||||
});
|
||||
|
||||
emailsSent++;
|
||||
console.log(`✅ Sent ${frequency} reminder email to ${userData.email}`);
|
||||
console.log(`✅ Sent daily summary email to ${userData.email}`);
|
||||
|
||||
} catch (emailError) {
|
||||
console.error(`❌ Failed to send email to ${userData.email}:`, emailError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📧 Scheduled email check complete. Sent ${emailsSent} emails.`);
|
||||
console.log(`📧 Daily email summary complete. Sent ${emailsSent} emails.`);
|
||||
return NextResponse.json({
|
||||
message: `Scheduled email check complete. Sent ${emailsSent} emails.`,
|
||||
message: `Daily email summary complete. Sent ${emailsSent} emails.`,
|
||||
emailsSent
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Scheduled email error:', error);
|
||||
return NextResponse.json({ error: 'Failed to process scheduled emails' }, { status: 500 });
|
||||
console.error('❌ Error details:', {
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
name: error instanceof Error ? error.name : 'Unknown'
|
||||
});
|
||||
return NextResponse.json({
|
||||
error: 'Failed to process scheduled emails',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
// POST handler calls the same logic as GET for testing purposes
|
||||
return GET();
|
||||
}
|
||||
Reference in new issue
Block a user