Implement scheduled email reminders
- Add cron service to Docker container for automated email sending - Create /api/send-scheduled-emails endpoint for cron job calls - Modify prayer submission to store user reminder preferences instead of sending immediate emails - Update Firestore rules to allow server-side API access for scheduled emails - Add userId to prayers for proper user association - Cron runs every 5 minutes to check and send scheduled reminders
This commit is contained in:
1 parent
220788ef70
commit
6b0713612d
5 files changed
+398
-35
No files matched your search
+19
-1
@@ -33,6 +33,12 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
|||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Install cron and curl for scheduled tasks
|
||||||
|
RUN apk add --no-cache dcron curl
|
||||||
|
|
||||||
|
# Create cron log directory
|
||||||
|
RUN mkdir -p /var/log/cron && chown nextjs:nodejs /var/log/cron
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
# Set the correct permission for prerender cache
|
# Set the correct permission for prerender cache
|
||||||
@@ -52,6 +58,18 @@ ENV PORT=3000
|
|||||||
# set hostname to localhost
|
# set hostname to localhost
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# Create cron jobs for scheduled emails
|
||||||
|
RUN echo "*/5 * * * * curl -s http://localhost:3000/api/send-scheduled-emails >> /var/log/cron/scheduled-emails.log 2>&1" > /etc/crontabs/nextjs
|
||||||
|
|
||||||
|
# Make sure cron files have correct permissions
|
||||||
|
RUN chown nextjs:nodejs /etc/crontabs/nextjs
|
||||||
|
|
||||||
|
# Create startup script
|
||||||
|
RUN echo '#!/bin/sh' > /app/start.sh && \
|
||||||
|
echo 'crond -f -l 8 &' >> /app/start.sh && \
|
||||||
|
echo 'exec "$@"' >> /app/start.sh && \
|
||||||
|
chmod +x /app/start.sh
|
||||||
|
|
||||||
# server.js is created by next build from the standalone output
|
# server.js is created by next build from the standalone output
|
||||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||||
CMD ["node", "server.js"]
|
CMD ["/app/start.sh", "node", "server.js"]
|
||||||
@@ -11,6 +11,16 @@ service cloud.firestore {
|
|||||||
allow read, write: if request.auth != null;
|
allow read, write: if request.auth != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow server-side API access for scheduled emails (no auth required for GET)
|
||||||
|
match /prayers {
|
||||||
|
allow read: if true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow server-side API access for scheduled emails
|
||||||
|
match /users {
|
||||||
|
allow read: if true;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow authenticated users to read/write config (for admin settings)
|
// Allow authenticated users to read/write config (for admin settings)
|
||||||
match /config/{configId} {
|
match /config/{configId} {
|
||||||
allow read, write: if request.auth != null;
|
allow read, write: if request.auth != null;
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,t)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>a(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(n.map(e=>o[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/84QbTf9EqwkRW6ZuK_UKV/_buildManifest.js",revision:"009de90521464c9665092678c4f82fe9"},{url:"/_next/static/84QbTf9EqwkRW6ZuK_UKV/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/240-754372c10c8766c9.js",revision:"754372c10c8766c9"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/646.30443a55a43a3bfb.js",revision:"30443a55a43a3bfb"},{url:"/_next/static/chunks/7508b87c-1ab0c72b2a9e57da.js",revision:"1ab0c72b2a9e57da"},{url:"/_next/static/chunks/app/_global-error/page-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/app/_not-found/page-1123cfe416c748d0.js",revision:"1123cfe416c748d0"},{url:"/_next/static/chunks/app/api/send-email/route-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/app/layout-dc9d7c3da9cf59ef.js",revision:"dc9d7c3da9cf59ef"},{url:"/_next/static/chunks/app/page-5ebd182a478eec74.js",revision:"5ebd182a478eec74"},{url:"/_next/static/chunks/dd8162e8-b0853216adf21a86.js",revision:"b0853216adf21a86"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-7ddb690de3906442.js",revision:"7ddb690de3906442"},{url:"/_next/static/chunks/main-app-4f19d69ed4295336.js",revision:"4f19d69ed4295336"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-bcba0732671665d2.js",revision:"bcba0732671665d2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-167d4419e1b22999.js",revision:"167d4419e1b22999"},{url:"/_next/static/css/8a0dbbd40cc7c3db.css",revision:"8a0dbbd40cc7c3db"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/manifest.json",revision:"785fbc5941efd3caa27ade80c67bb5ef"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:a,state:n})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jLine truncated
|
if(!self.define){let e,s={};const n=(n,a)=>(n=new URL(n+".js",a).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(a,t)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>n(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(a.map(e=>o[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/240-754372c10c8766c9.js",revision:"754372c10c8766c9"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/646.30443a55a43a3bfb.js",revision:"30443a55a43a3bfb"},{url:"/_next/static/chunks/7508b87c-1ab0c72b2a9e57da.js",revision:"1ab0c72b2a9e57da"},{url:"/_next/static/chunks/app/_global-error/page-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/_not-found/page-1123cfe416c748d0.js",revision:"1123cfe416c748d0"},{url:"/_next/static/chunks/app/api/send-email/route-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/api/send-scheduled-emails/route-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/layout-dc9d7c3da9cf59ef.js",revision:"dc9d7c3da9cf59ef"},{url:"/_next/static/chunks/app/page-99ceb58c98b8f8c5.js",revision:"99ceb58c98b8f8c5"},{url:"/_next/static/chunks/dd8162e8-b0853216adf21a86.js",revision:"b0853216adf21a86"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-7ddb690de3906442.js",revision:"7ddb690de3906442"},{url:"/_next/static/chunks/main-app-4f19d69ed4295336.js",revision:"4f19d69ed4295336"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-bcba0732671665d2.js",revision:"bcba0732671665d2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-167d4419e1b22999.js",revision:"167d4419e1b22999"},{url:"/_next/static/cnkXhILcfrDgcR4Vc6_X1/_buildManifest.js",revision:"a91cb247fd17096d2ba17603cfdf5fe7"},{url:"/_next/static/cnkXhILcfrDgcR4Vc6_X1/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/css/40e47386a48391ef.css",revision:"40e47386a48391ef"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/manifest.json",revision:"785fbc5941efd3caa27ade80c67bb5ef"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:a})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-aLine truncated
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { doc, getDoc, collection, getDocs, updateDoc } from 'firebase/firestore';
|
||||||
|
import { db } from '@/lib/firebase';
|
||||||
|
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
function generatePrayerEmailHTML(message: string, subject: string) {
|
||||||
|
// Parse the message to extract prayer details
|
||||||
|
const lines = message.split('\n');
|
||||||
|
let currentPrayer = null;
|
||||||
|
let prayers = [];
|
||||||
|
let summarySection = 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"
|
||||||
|
const match = line.match(/(\d{4}-\d{2}-\d{2}) - (PRAYER|PRAISE): (.+)/);
|
||||||
|
if (match) {
|
||||||
|
prayers.push({
|
||||||
|
type: 'summary-item',
|
||||||
|
date: match[1],
|
||||||
|
prayerType: match[2].toLowerCase(),
|
||||||
|
text: match[3]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (currentPrayer && !currentPrayer.text && line.trim()) {
|
||||||
|
currentPrayer.text = line;
|
||||||
|
prayers.push(currentPrayer);
|
||||||
|
currentPrayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${subject}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.header .subtitle {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
.prayer-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.prayer-type {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.prayer-date {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.prayer-text {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.prayer-journal {
|
||||||
|
background: #fff;
|
||||||
|
border-left: 3px solid #764ba2;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.pray-for {
|
||||||
|
background: #e8f4fd;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c5282;
|
||||||
|
}
|
||||||
|
.summary-section {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.summary-section h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.summary-item {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.summary-item .date {
|
||||||
|
color: #ffed4e;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.summary-item .prayer-type {
|
||||||
|
color: #ffed4e;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.verse {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.verse blockquote {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
.verse cite {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🙏 Prayer App</h1>
|
||||||
|
<div class="subtitle">${subject}</div>
|
||||||
|
</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('')}
|
||||||
|
|
||||||
|
<div class="verse">
|
||||||
|
<blockquote>"Pray without ceasing"</blockquote>
|
||||||
|
<cite>— 1 Thessalonians 5:17</cite>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Sent with ❤️ from your Prayer App</p>
|
||||||
|
<p>This email helps you stay connected to your spiritual journey.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
console.log('❌ SMTP configuration not found');
|
||||||
|
return NextResponse.json({ message: 'SMTP configuration not found' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const smtpConfig = configDoc.data();
|
||||||
|
|
||||||
|
// Create transporter
|
||||||
|
const transporter = nodemailer.createTransporter({
|
||||||
|
host: smtpConfig.host,
|
||||||
|
port: smtpConfig.port,
|
||||||
|
secure: smtpConfig.secure,
|
||||||
|
auth: {
|
||||||
|
user: smtpConfig.user,
|
||||||
|
pass: smtpConfig.pass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all users who have reminder preferences
|
||||||
|
const usersRef = collection(db, 'users');
|
||||||
|
const usersSnapshot = await getDocs(usersRef);
|
||||||
|
|
||||||
|
let emailsSent = 0;
|
||||||
|
|
||||||
|
for (const userDoc of usersSnapshot.docs) {
|
||||||
|
const userData = userDoc.data();
|
||||||
|
const userId = userDoc.id;
|
||||||
|
|
||||||
|
// Check if user has reminder preferences
|
||||||
|
if (!userData.reminderFrequency || userData.reminderFrequency === 'never') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check last email sent time
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
if (userPrayers.length === 0) {
|
||||||
|
continue; // No active prayers to send
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const subject = `Your ${frequency} prayer reminder - ${userPrayers.length} active prayers`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send email
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: smtpConfig.from,
|
||||||
|
to: userData.email,
|
||||||
|
subject,
|
||||||
|
text: message,
|
||||||
|
html: generatePrayerEmailHTML(message, subject),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update last email sent time
|
||||||
|
await updateDoc(doc(db, 'users', userId), {
|
||||||
|
lastEmailSent: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
emailsSent++;
|
||||||
|
console.log(`✅ Sent ${frequency} reminder 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.`);
|
||||||
|
return NextResponse.json({
|
||||||
|
message: `Scheduled email check complete. Sent ${emailsSent} emails.`,
|
||||||
|
emailsSent
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Scheduled email error:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to process scheduled emails' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-33
@@ -4,9 +4,12 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import Login from '@/components/Login';
|
import Login from '@/components/Login';
|
||||||
import Admin from '@/components/Admin';
|
import Admin from '@/components/Admin';
|
||||||
|
import { db } from '@/lib/firebase';
|
||||||
|
import { doc, setDoc } from 'firebase/firestore';
|
||||||
|
|
||||||
interface Prayer {
|
interface Prayer {
|
||||||
id: string;
|
id: string;
|
||||||
|
userId?: string;
|
||||||
date: string;
|
date: string;
|
||||||
type: 'prayer' | 'praise';
|
type: 'prayer' | 'praise';
|
||||||
text: string;
|
text: string;
|
||||||
@@ -69,49 +72,28 @@ export default function Home() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newPrayer: Prayer = {
|
const newPrayer: Prayer = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
|
userId: user?.uid || 'anonymous',
|
||||||
...form,
|
...form,
|
||||||
};
|
};
|
||||||
setPrayers(prev => [...prev, newPrayer]);
|
setPrayers(prev => [...prev, newPrayer]);
|
||||||
|
|
||||||
if (form.email && form.reminderFrequency !== 'never') {
|
// Store user reminder preferences in Firestore
|
||||||
|
if (user && form.reminderFrequency !== 'never') {
|
||||||
try {
|
try {
|
||||||
let message = `Date: ${form.date}\n\n${form.text}\n\nJournal: ${form.journal || 'None'}`;
|
await setDoc(doc(db, 'users', user.uid), {
|
||||||
|
email: form.email,
|
||||||
|
reminderFrequency: form.reminderFrequency,
|
||||||
|
includeActiveSummary: form.includeActiveSummary,
|
||||||
|
lastEmailSent: null, // Will be set when first email is sent
|
||||||
|
}, { merge: true });
|
||||||
|
|
||||||
if (form.prayFor) {
|
console.log('✅ Reminder preferences saved');
|
||||||
message += `\n\nPraying for: ${form.prayFor}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (form.includeActiveSummary) {
|
|
||||||
const activePrayers = prayers.filter(prayer => !prayer.archived);
|
|
||||||
if (activePrayers.length > 0) {
|
|
||||||
message += `\n\n--- Active Prayers Summary ---\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`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch('/api/send-email', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: form.email,
|
|
||||||
subject: `New ${form.type} added - ${form.reminderFrequency} reminders enabled`,
|
|
||||||
message: message,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send email:', error);
|
console.error('❌ Failed to save reminder preferences:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset form
|
||||||
setForm({
|
setForm({
|
||||||
date: new Date().toISOString().split('T')[0],
|
date: new Date().toISOString().split('T')[0],
|
||||||
type: 'prayer',
|
type: 'prayer',
|
||||||
|
|||||||
Reference in new issue
Block a user