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
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import Admin from '@/components/Admin';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function AdminPage() {
|
||||
const { user, isAdmin } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
router.push('/');
|
||||
} else if (!isAdmin) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [user, isAdmin, router]);
|
||||
|
||||
if (!user || !isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white">
|
||||
<Navbar />
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<Admin />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Debug API endpoint to check SMTP config server-side
|
||||
// This will work even without client authentication
|
||||
|
||||
import { db } from '@/lib/firebase';
|
||||
import { doc, getDoc } from 'firebase/firestore';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const configDoc = await getDoc(doc(db, 'config', 'smtp'));
|
||||
|
||||
if (configDoc.exists()) {
|
||||
const config = configDoc.data();
|
||||
return Response.json({
|
||||
found: true,
|
||||
host: config?.host,
|
||||
port: config?.port,
|
||||
secure: config?.secure,
|
||||
user: config?.user ? 'SET' : 'MISSING',
|
||||
pass: config?.pass ? 'SET' : 'MISSING',
|
||||
from: config?.from,
|
||||
hasAllFields: !!(config?.host && config?.port && config?.user && config?.pass && config?.from)
|
||||
});
|
||||
} else {
|
||||
return Response.json({ found: false, error: 'SMTP config not found' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
return Response.json({ error: error.message });
|
||||
}
|
||||
}
|
||||
@@ -234,13 +234,38 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, subject, message } = await request.json();
|
||||
|
||||
console.log('📧 Attempting to send email to:', email);
|
||||
|
||||
// Get SMTP config from Firestore
|
||||
console.log('📧 Fetching SMTP config from Firestore...');
|
||||
const configDoc = await getDoc(doc(db, 'config', 'smtp'));
|
||||
|
||||
if (!configDoc.exists()) {
|
||||
return NextResponse.json({ error: 'SMTP configuration not found' }, { status: 500 });
|
||||
console.error('❌ SMTP configuration not found in Firestore');
|
||||
return NextResponse.json({
|
||||
error: 'SMTP configuration not found',
|
||||
details: 'Please configure SMTP settings in the Admin panel at /admin'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
const smtpConfig = configDoc.data();
|
||||
console.log('✅ SMTP config found, host:', smtpConfig?.host);
|
||||
|
||||
// Validate SMTP config
|
||||
if (!smtpConfig?.host || !smtpConfig?.user || !smtpConfig?.pass || !smtpConfig?.from) {
|
||||
console.error('❌ SMTP config incomplete:', {
|
||||
hasHost: !!smtpConfig?.host,
|
||||
hasUser: !!smtpConfig?.user,
|
||||
hasPass: !!smtpConfig?.pass,
|
||||
hasFrom: !!smtpConfig?.from
|
||||
});
|
||||
return NextResponse.json({
|
||||
error: 'SMTP configuration incomplete',
|
||||
details: 'Missing required SMTP fields. Please check Admin panel.'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
console.log('📧 Creating email transporter...');
|
||||
|
||||
// Create transporter with dynamic config
|
||||
const transporter = nodemailer.createTransport({
|
||||
@@ -253,8 +278,10 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('📧 Sending email...');
|
||||
|
||||
// Send email
|
||||
await transporter.sendMail({
|
||||
const result = await transporter.sendMail({
|
||||
from: smtpConfig.from,
|
||||
to: email,
|
||||
subject,
|
||||
@@ -262,9 +289,37 @@ export async function POST(request: NextRequest) {
|
||||
html: generatePrayerEmailHTML(message, subject),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Email send error:', error);
|
||||
return NextResponse.json({ error: 'Failed to send email' }, { status: 500 });
|
||||
console.log('✅ Email sent successfully:', result.messageId);
|
||||
return NextResponse.json({ success: true, messageId: result.messageId });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Email send error:', error);
|
||||
|
||||
// Provide more specific error messages
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
return NextResponse.json({
|
||||
error: 'SMTP connection failed',
|
||||
details: 'Cannot connect to SMTP server. Check host and port.'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
if (error.code === 'EAUTH') {
|
||||
return NextResponse.json({
|
||||
error: 'SMTP authentication failed',
|
||||
details: 'Invalid username or password. For Gmail, use App Password.'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
if (error.code === 'ESOCKET') {
|
||||
return NextResponse.json({
|
||||
error: 'SMTP socket error',
|
||||
details: 'Network or firewall issue. Check internet connection.'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
error: 'Failed to send email',
|
||||
details: error.message || 'Unknown error occurred'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -24,3 +24,18 @@ body {
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
+5
-8
@@ -1,7 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import ClientLayout from "@/components/ClientLayout";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -30,15 +29,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning={true}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
<ClientLayout>
|
||||
{children}
|
||||
</ClientLayout>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+679
-348
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import Profile from '@/components/Profile';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [user, router]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white">
|
||||
<Navbar />
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<Profile />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { collection, query, where, orderBy, getDocs } from 'firebase/firestore';
|
||||
import { db } from '@/lib/firebase';
|
||||
import Navbar from '@/components/Navbar';
|
||||
|
||||
interface Testimony {
|
||||
id: string;
|
||||
text: string;
|
||||
userId: string;
|
||||
answeredDate: string;
|
||||
tags?: string[];
|
||||
type: 'prayer' | 'praise';
|
||||
prayFor?: string;
|
||||
}
|
||||
|
||||
export default function TestimoniesPage() {
|
||||
const [testimonies, setTestimonies] = useState<Testimony[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState<'all' | 'prayer' | 'praise'>('all');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTestimonies = async () => {
|
||||
try {
|
||||
const q = query(
|
||||
collection(db, 'prayers'),
|
||||
where('answered', '==', true),
|
||||
orderBy('answeredDate', 'desc')
|
||||
);
|
||||
const querySnapshot = await getDocs(q);
|
||||
const testimoniesData = querySnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data()
|
||||
})) as Testimony[];
|
||||
setTestimonies(testimoniesData);
|
||||
} catch (error) {
|
||||
console.error('Error fetching testimonies:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTestimonies();
|
||||
}, []);
|
||||
|
||||
const filteredTestimonies = testimonies.filter(testimony =>
|
||||
filter === 'all' || testimony.type === filter
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white">
|
||||
<Navbar />
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="bg-gray-800/50 rounded-lg p-6">
|
||||
<div className="h-4 bg-gray-700 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-3 bg-gray-700 rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white">
|
||||
<Navbar />
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4">🙏 Testimony Wall</h1>
|
||||
<p className="text-gray-300">Celebrating answered prayers and praises</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Buttons */}
|
||||
<div className="flex justify-center space-x-4 mb-8">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === 'all'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
All Testimonies
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('prayer')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === 'prayer'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
🙏 Prayers
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('praise')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === 'praise'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
🙌 Praises
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{filteredTestimonies.map((testimony) => (
|
||||
<div key={testimony.id} className="bg-gray-800/50 backdrop-blur-sm rounded-2xl p-6 shadow-2xl border border-gray-700/50">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-2xl">{testimony.type === 'prayer' ? '🙏' : '🙌'}</span>
|
||||
<span className="text-green-400 font-semibold">
|
||||
Answered {testimony.type === 'prayer' ? 'Prayer' : 'Praise'}
|
||||
</span>
|
||||
{testimony.prayFor && (
|
||||
<span className="text-sm text-gray-400">for {testimony.prayFor}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">
|
||||
{new Date(testimony.answeredDate).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-200 mb-4 leading-relaxed">{testimony.text}</p>
|
||||
|
||||
{testimony.tags && testimony.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{testimony.tags.map((tag, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-600/20 text-blue-300 rounded-full text-xs">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredTestimonies.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🙏</div>
|
||||
<h2 className="text-2xl font-semibold mb-2">No Testimonies Yet</h2>
|
||||
<p className="text-gray-400">
|
||||
{filter === 'all'
|
||||
? 'Answered prayers will appear here to inspire others.'
|
||||
: `No answered ${filter === 'prayer' ? 'prayers' : 'praises'} yet.`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
export default function ClientLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#1f2937',
|
||||
color: '#fff',
|
||||
border: '1px solid #374151',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { db } from '../lib/firebase';
|
||||
import { collection, addDoc, updateDoc, deleteDoc, doc, getDocs, query, where } from 'firebase/firestore';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
interface EmailTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
type: 'prayer_update' | 'reminder' | 'summary';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface EmailTemplatesManagerProps {
|
||||
onTemplateSelect?: (templateId: string) => void;
|
||||
selectedTemplateId?: string;
|
||||
}
|
||||
|
||||
export default function EmailTemplatesManager({ onTemplateSelect, selectedTemplateId }: EmailTemplatesManagerProps) {
|
||||
const { user } = useAuth();
|
||||
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
type: 'prayer_update' as EmailTemplate['type']
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadTemplates();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const q = query(collection(db, 'emailTemplates'), where('userId', '==', user.uid));
|
||||
const querySnapshot = await getDocs(q);
|
||||
const templatesData = querySnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data()
|
||||
})) as EmailTemplate[];
|
||||
setTemplates(templatesData);
|
||||
} catch (error) {
|
||||
console.error('Error loading email templates:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const templateData = {
|
||||
...form,
|
||||
userId: user.uid,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (editingTemplate) {
|
||||
await updateDoc(doc(db, 'emailTemplates', editingTemplate.id), templateData);
|
||||
} else {
|
||||
await addDoc(collection(db, 'emailTemplates'), {
|
||||
...templateData,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
setForm({ name: '', subject: '', body: '', type: 'prayer_update' });
|
||||
setShowForm(false);
|
||||
setEditingTemplate(null);
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error saving email template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (template: EmailTemplate) => {
|
||||
setEditingTemplate(template);
|
||||
setForm({
|
||||
name: template.name,
|
||||
subject: template.subject,
|
||||
body: template.body,
|
||||
type: template.type
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (templateId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this email template?')) return;
|
||||
|
||||
try {
|
||||
await deleteDoc(doc(db, 'emailTemplates', templateId));
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error deleting email template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowForm(false);
|
||||
setEditingTemplate(null);
|
||||
setForm({ name: '', subject: '', body: '', type: 'prayer_update' });
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: EmailTemplate['type']) => {
|
||||
switch (type) {
|
||||
case 'prayer_update': return 'Prayer Update';
|
||||
case 'reminder': return 'Reminder';
|
||||
case 'summary': return 'Summary';
|
||||
default: return type;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeColor = (type: EmailTemplate['type']) => {
|
||||
switch (type) {
|
||||
case 'prayer_update': return 'bg-blue-600';
|
||||
case 'reminder': return 'bg-orange-600';
|
||||
case 'summary': return 'bg-green-600';
|
||||
default: return 'bg-gray-600';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-white">Loading email templates...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">📧 Email Templates</h2>
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
+ New Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<form onSubmit={handleSubmit} className="mb-6 p-4 bg-gray-700 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
{editingTemplate ? 'Edit Email Template' : 'Create New Email Template'}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Template Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={e => setForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., Weekly Prayer Update"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Template Type</label>
|
||||
<select
|
||||
value={form.type}
|
||||
onChange={e => setForm(prev => ({ ...prev, type: e.target.value as EmailTemplate['type'] }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="prayer_update">Prayer Update</option>
|
||||
<option value="reminder">Reminder</option>
|
||||
<option value="summary">Summary</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Email Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.subject}
|
||||
onChange={e => setForm(prev => ({ ...prev, subject: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., Your Weekly Prayer Update"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Email Body</label>
|
||||
<textarea
|
||||
value={form.body}
|
||||
onChange={e => setForm(prev => ({ ...prev, body: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[150px] resize-none"
|
||||
placeholder={`Write your email template here. Available placeholders:
|
||||
{userName} - User's name
|
||||
{prayerText} - The prayer text
|
||||
{prayFor} - Who they're praying for
|
||||
{date} - Date of prayer
|
||||
{type} - Prayer or praise`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-400 mb-4">
|
||||
<strong>Available placeholders:</strong> {'{userName}'}, {'{prayerText}'}, {'{prayFor}'}, {'{date}'}, {'{type}'}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{editingTemplate ? 'Update' : 'Create'} Template
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{templates.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
No email templates created yet. Create your first template to get started!
|
||||
</div>
|
||||
) : (
|
||||
templates.map(template => (
|
||||
<div key={template.id} className="p-4 bg-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<h3 className="text-lg font-semibold text-white">{template.name}</h3>
|
||||
<span className={`px-2 py-1 text-white text-xs rounded-full ${getTypeColor(template.type)}`}>
|
||||
{getTypeLabel(template.type)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-300 text-sm mb-2">
|
||||
<strong>Subject:</strong> {template.subject}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{onTemplateSelect && (
|
||||
<button
|
||||
onClick={() => onTemplateSelect(template.id)}
|
||||
className={`px-3 py-1 rounded text-xs transition-colors ${
|
||||
selectedTemplateId === template.id
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{selectedTemplateId === template.id ? 'Selected' : 'Select'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleEdit(template)}
|
||||
className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(template.id)}
|
||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-600 p-3 rounded text-gray-200 text-sm whitespace-pre-wrap max-h-32 overflow-y-auto">
|
||||
{template.body}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 bg-gray-700 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-700 rounded w-1/2"></div>
|
||||
<div className="h-4 bg-gray-700 rounded w-5/6"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+82
-48
@@ -9,15 +9,18 @@ export default function Login() {
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isSignup, setIsSignup] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login, signup } = useAuth();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
if (isSignup) {
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -32,62 +35,93 @@ export default function Login() {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
||||
<form onSubmit={handleSubmit} className="bg-gray-800 p-8 rounded-lg w-full max-w-md">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">
|
||||
{isSignup ? 'Sign Up for Prayer App' : 'Login to Prayer App'}
|
||||
</h1>
|
||||
{error && <p className="text-red-500 mb-4">{error}</p>}
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded"
|
||||
required
|
||||
/>
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-black text-white flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold mb-2">Prayer App</h1>
|
||||
<p className="text-gray-400">Manage your spiritual journey</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{isSignup && (
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2">Confirm Password</label>
|
||||
<form onSubmit={handleSubmit} className="bg-gray-800 p-8 rounded-xl shadow-2xl space-y-6">
|
||||
<h2 className="text-2xl font-semibold text-center">
|
||||
{isSignup ? 'Create Account' : 'Welcome Back'}
|
||||
</h2>
|
||||
{error && (
|
||||
<div className="bg-red-900 border border-red-700 text-red-200 px-4 py-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoFocus
|
||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" className="w-full p-2 bg-blue-600 hover:bg-blue-700 rounded mb-4">
|
||||
{isSignup ? 'Sign Up' : 'Login'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsSignup(!isSignup);
|
||||
setError('');
|
||||
setConfirmPassword('');
|
||||
}}
|
||||
className="w-full p-2 bg-gray-600 hover:bg-gray-700 rounded"
|
||||
>
|
||||
{isSignup ? 'Already have an account? Login' : 'Need an account? Sign Up'}
|
||||
</button>
|
||||
</form>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{isSignup && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
placeholder="Confirm your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full p-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed rounded-lg font-medium transition-colors flex items-center justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
isSignup ? 'Create Account' : 'Sign In'
|
||||
)}
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsSignup(!isSignup);
|
||||
setError('');
|
||||
setConfirmPassword('');
|
||||
}}
|
||||
className="text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
{isSignup ? 'Already have an account? Sign In' : 'Need an account? Sign Up'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab?: string;
|
||||
onTabChange?: (tab: string) => void;
|
||||
onSearchClear?: () => void;
|
||||
onPageReset?: () => void;
|
||||
}
|
||||
|
||||
export default function Navbar({ activeTab, onTabChange, onSearchClear, onPageReset }: NavbarProps) {
|
||||
const { user, logout, isAdmin } = useAuth();
|
||||
const router = useRouter();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const navigationTabs = [
|
||||
{ key: 'dashboard', label: 'Dashboard', icon: '📊' },
|
||||
{ key: 'add', label: 'Add Prayer', icon: '➕' },
|
||||
{ key: 'active', label: 'Active', icon: '📖' },
|
||||
{ key: 'archived', label: 'Archived', icon: '📂' },
|
||||
{ key: 'search', label: 'Search', icon: '🔍' },
|
||||
{ key: 'community', label: 'Community', icon: '🌍' },
|
||||
{ key: 'organization', label: 'Organization', icon: '🗂️' },
|
||||
{ key: 'testimonies', label: 'Testimonies', icon: '🙌' },
|
||||
];
|
||||
|
||||
const handleTabChange = (tabKey: string) => {
|
||||
if (tabKey === 'testimonies') {
|
||||
router.push('/testimonies');
|
||||
setIsMobileMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onTabChange) {
|
||||
onTabChange(tabKey);
|
||||
}
|
||||
if (tabKey !== 'search' && onSearchClear) {
|
||||
onSearchClear();
|
||||
}
|
||||
if (onPageReset) {
|
||||
onPageReset();
|
||||
}
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-900/95 backdrop-blur-sm border-b border-gray-800/50 px-4 py-3 shadow-lg">
|
||||
<div className="max-w-6xl mx-auto flex items-center justify-between">
|
||||
{/* Logo/Brand */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">🙏</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleTabChange('dashboard');
|
||||
router.push('/');
|
||||
}}
|
||||
className="text-lg md:text-xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent hover:from-blue-300 hover:to-purple-400 transition-colors"
|
||||
>
|
||||
Prayer App
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation - Hidden on mobile */}
|
||||
<div className="hidden lg:flex items-center space-x-2">
|
||||
{navigationTabs.map(({ key, label, icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => handleTabChange(key)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center space-x-2 text-sm ${
|
||||
activeTab === key
|
||||
? 'bg-blue-600 text-white shadow-lg'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-800/50'
|
||||
}`}
|
||||
>
|
||||
<span>{icon}</span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
))}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => router.push('/admin')}
|
||||
className="text-gray-300 hover:text-white transition-colors px-3 py-2 rounded-lg hover:bg-gray-800/50 text-sm"
|
||||
>
|
||||
Admin
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => router.push('/profile')}
|
||||
className="text-gray-300 hover:text-white transition-colors px-3 py-2 rounded-lg hover:bg-gray-800/50 text-sm"
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="bg-red-600/20 hover:bg-red-600/30 text-red-400 hover:text-red-300 px-4 py-2 rounded-lg transition-all duration-200 border border-red-500/30 text-sm"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="lg:hidden">
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="text-gray-300 hover:text-white p-2 rounded-lg hover:bg-gray-800/50 transition-colors"
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="lg:hidden border-t border-gray-800/50 mt-3 pt-3">
|
||||
<div className="flex flex-col space-y-1">
|
||||
{/* Main Navigation Tabs */}
|
||||
{navigationTabs.map(({ key, label, icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => handleTabChange(key)}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center space-x-3 ${
|
||||
activeTab === key
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-800/50'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{icon}</span>
|
||||
<span className="font-medium">{label}</span>
|
||||
{activeTab === key && (
|
||||
<span className="ml-auto text-xs bg-blue-500/20 text-blue-300 px-2 py-1 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-gray-700 my-2"></div>
|
||||
|
||||
{/* Additional Options */}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push('/admin');
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full text-left text-gray-300 hover:text-white transition-colors px-4 py-3 rounded-lg hover:bg-gray-800/50 flex items-center space-x-3"
|
||||
>
|
||||
<span className="text-lg">⚙️</span>
|
||||
<span>Admin Panel</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push('/profile');
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full text-left text-gray-300 hover:text-white transition-colors px-4 py-3 rounded-lg hover:bg-gray-800/50 flex items-center space-x-3"
|
||||
>
|
||||
<span className="text-lg">👤</span>
|
||||
<span>Profile</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full text-left bg-red-600/20 hover:bg-red-600/30 text-red-400 hover:text-red-300 px-4 py-3 rounded-lg transition-all duration-200 border border-red-500/30 flex items-center space-x-3"
|
||||
>
|
||||
<span className="text-lg">🚪</span>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Prayer {
|
||||
id: string;
|
||||
text: string;
|
||||
type: 'prayer' | 'praise';
|
||||
answered?: boolean;
|
||||
chainId?: string;
|
||||
chainOrder?: number;
|
||||
}
|
||||
|
||||
interface PrayerChainProps {
|
||||
prayers: Prayer[];
|
||||
onChainClick: (chainId: string) => void;
|
||||
}
|
||||
|
||||
export default function PrayerChain({ prayers, onChainClick }: PrayerChainProps) {
|
||||
const chains = prayers.reduce((acc, prayer) => {
|
||||
if (prayer.chainId) {
|
||||
if (!acc[prayer.chainId]) acc[prayer.chainId] = [];
|
||||
acc[prayer.chainId].push(prayer);
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, Prayer[]>);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(chains).map(([chainId, chainPrayers]) => (
|
||||
<div key={chainId} className="bg-gray-800/50 rounded-lg p-4 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-2 text-white">Prayer Chain</h3>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{chainPrayers
|
||||
.sort((a, b) => (a.chainOrder || 0) - (b.chainOrder || 0))
|
||||
.map((prayer, index) => (
|
||||
<div key={prayer.id} className="flex items-center space-x-2">
|
||||
<div className="w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center text-xs text-white font-bold">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 text-sm text-gray-200 truncate">{prayer.text}</div>
|
||||
{prayer.answered && <span className="text-green-400">✓</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onChainClick(chainId)}
|
||||
className="mt-2 text-blue-400 hover:text-blue-300 text-sm underline"
|
||||
>
|
||||
View Full Chain
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { db } from '../lib/firebase';
|
||||
import { collection, addDoc, updateDoc, deleteDoc, doc, getDocs, query, where } from 'firebase/firestore';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
interface PrayerGoal {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
targetCount: number;
|
||||
currentCount: number;
|
||||
category: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isCompleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface PrayerGoalsManagerProps {
|
||||
onGoalSelect?: (goalId: string) => void;
|
||||
selectedGoalId?: string;
|
||||
}
|
||||
|
||||
export default function PrayerGoalsManager({ onGoalSelect, selectedGoalId }: PrayerGoalsManagerProps) {
|
||||
const { user } = useAuth();
|
||||
const [goals, setGoals] = useState<PrayerGoal[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingGoal, setEditingGoal] = useState<PrayerGoal | null>(null);
|
||||
const [form, setForm] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
targetCount: 1,
|
||||
category: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadGoals();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadGoals = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const q = query(collection(db, 'prayerGoals'), where('userId', '==', user.uid));
|
||||
const querySnapshot = await getDocs(q);
|
||||
const goalsData = querySnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data()
|
||||
})) as PrayerGoal[];
|
||||
setGoals(goalsData);
|
||||
} catch (error) {
|
||||
console.error('Error loading goals:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const goalData = {
|
||||
...form,
|
||||
currentCount: editingGoal ? editingGoal.currentCount : 0,
|
||||
isCompleted: false,
|
||||
userId: user.uid,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (editingGoal) {
|
||||
await updateDoc(doc(db, 'prayerGoals', editingGoal.id), goalData);
|
||||
} else {
|
||||
await addDoc(collection(db, 'prayerGoals'), {
|
||||
...goalData,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
setForm({ title: '', description: '', targetCount: 1, category: '', startDate: '', endDate: '' });
|
||||
setShowForm(false);
|
||||
setEditingGoal(null);
|
||||
loadGoals();
|
||||
} catch (error) {
|
||||
console.error('Error saving goal:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (goal: PrayerGoal) => {
|
||||
setEditingGoal(goal);
|
||||
setForm({
|
||||
title: goal.title,
|
||||
description: goal.description,
|
||||
targetCount: goal.targetCount,
|
||||
category: goal.category,
|
||||
startDate: goal.startDate,
|
||||
endDate: goal.endDate
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (goalId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this goal?')) return;
|
||||
|
||||
try {
|
||||
await deleteDoc(doc(db, 'prayerGoals', goalId));
|
||||
loadGoals();
|
||||
} catch (error) {
|
||||
console.error('Error deleting goal:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowForm(false);
|
||||
setEditingGoal(null);
|
||||
setForm({ title: '', description: '', targetCount: 1, category: '', startDate: '', endDate: '' });
|
||||
};
|
||||
|
||||
const getProgressPercentage = (current: number, target: number) => {
|
||||
return Math.min((current / target) * 100, 100);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-white">Loading goals...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">🎯 Prayer Goals</h2>
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
+ New Goal
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<form onSubmit={handleSubmit} className="mb-6 p-4 bg-gray-700 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
{editingGoal ? 'Edit Goal' : 'Create New Goal'}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Goal Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={e => setForm(prev => ({ ...prev, title: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., Pray for 30 days"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Target Count</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={form.targetCount}
|
||||
onChange={e => setForm(prev => ({ ...prev, targetCount: parseInt(e.target.value) || 1 }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., 30"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.category}
|
||||
onChange={e => setForm(prev => ({ ...prev, category: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., Personal, Family, Ministry"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={e => setForm(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[80px] resize-none"
|
||||
placeholder="Describe your prayer goal..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.startDate}
|
||||
onChange={e => setForm(prev => ({ ...prev, startDate: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">End Date (Optional)</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.endDate}
|
||||
onChange={e => setForm(prev => ({ ...prev, endDate: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{editingGoal ? 'Update' : 'Create'} Goal
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{goals.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
No goals created yet. Create your first prayer goal to get started!
|
||||
</div>
|
||||
) : (
|
||||
goals.map(goal => {
|
||||
const progressPercent = getProgressPercentage(goal.currentCount, goal.targetCount);
|
||||
return (
|
||||
<div key={goal.id} className="p-4 bg-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<h3 className="text-lg font-semibold text-white">{goal.title}</h3>
|
||||
{goal.isCompleted && (
|
||||
<span className="px-2 py-1 bg-green-600 text-white text-xs rounded-full">
|
||||
Completed
|
||||
</span>
|
||||
)}
|
||||
{goal.category && (
|
||||
<span className="px-2 py-1 bg-blue-600 text-white text-xs rounded-full">
|
||||
{goal.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{goal.description && (
|
||||
<p className="text-gray-300 text-sm mb-3">{goal.description}</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-400">
|
||||
<span>Progress: {goal.currentCount} / {goal.targetCount}</span>
|
||||
{goal.startDate && <span>Started: {new Date(goal.startDate).toLocaleDateString()}</span>}
|
||||
{goal.endDate && <span>Ends: {new Date(goal.endDate).toLocaleDateString()}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{onGoalSelect && (
|
||||
<button
|
||||
onClick={() => onGoalSelect(goal.id)}
|
||||
className={`px-3 py-1 rounded text-xs transition-colors ${
|
||||
selectedGoalId === goal.id
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{selectedGoalId === goal.id ? 'Selected' : 'Select'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleEdit(goal)}
|
||||
className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(goal.id)}
|
||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-600 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="text-right text-xs text-gray-400 mt-1">
|
||||
{Math.round(progressPercent)}% Complete
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { db } from '../lib/firebase';
|
||||
import { collection, addDoc, updateDoc, deleteDoc, doc, getDocs, query, where } from 'firebase/firestore';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
interface PrayerTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
templateText: string;
|
||||
category: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface PrayerTemplatesManagerProps {
|
||||
onTemplateSelect?: (templateId: string) => void;
|
||||
selectedTemplateId?: string;
|
||||
}
|
||||
|
||||
export default function PrayerTemplatesManager({ onTemplateSelect, selectedTemplateId }: PrayerTemplatesManagerProps) {
|
||||
const { user } = useAuth();
|
||||
const [templates, setTemplates] = useState<PrayerTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<PrayerTemplate | null>(null);
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
templateText: '',
|
||||
category: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadTemplates();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const q = query(collection(db, 'prayerTemplates'), where('userId', '==', user.uid));
|
||||
const querySnapshot = await getDocs(q);
|
||||
const templatesData = querySnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data()
|
||||
})) as PrayerTemplate[];
|
||||
setTemplates(templatesData);
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const templateData = {
|
||||
...form,
|
||||
userId: user.uid,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (editingTemplate) {
|
||||
await updateDoc(doc(db, 'prayerTemplates', editingTemplate.id), templateData);
|
||||
} else {
|
||||
await addDoc(collection(db, 'prayerTemplates'), {
|
||||
...templateData,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
setForm({ name: '', description: '', templateText: '', category: '' });
|
||||
setShowForm(false);
|
||||
setEditingTemplate(null);
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error saving template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (template: PrayerTemplate) => {
|
||||
setEditingTemplate(template);
|
||||
setForm({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
templateText: template.templateText,
|
||||
category: template.category
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (templateId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this template?')) return;
|
||||
|
||||
try {
|
||||
await deleteDoc(doc(db, 'prayerTemplates', templateId));
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error deleting template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowForm(false);
|
||||
setEditingTemplate(null);
|
||||
setForm({ name: '', description: '', templateText: '', category: '' });
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-white">Loading templates...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">📋 Prayer Templates</h2>
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
+ New Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<form onSubmit={handleSubmit} className="mb-6 p-4 bg-gray-700 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
{editingTemplate ? 'Edit Template' : 'Create New Template'}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Template Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={e => setForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., Healing Prayer"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.category}
|
||||
onChange={e => setForm(prev => ({ ...prev, category: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., Health, Family, Work"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.description}
|
||||
onChange={e => setForm(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Brief description of this template"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Template Text</label>
|
||||
<textarea
|
||||
value={form.templateText}
|
||||
onChange={e => setForm(prev => ({ ...prev, templateText: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-600 border border-gray-500 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[120px] resize-none"
|
||||
placeholder="Write your prayer template here. Use {name} for placeholders..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{editingTemplate ? 'Update' : 'Create'} Template
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{templates.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
No templates created yet. Create your first template to get started!
|
||||
</div>
|
||||
) : (
|
||||
templates.map(template => (
|
||||
<div key={template.id} className="p-4 bg-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white">{template.name}</h3>
|
||||
{template.category && (
|
||||
<span className="inline-block px-2 py-1 bg-blue-600 text-white text-xs rounded-full mt-1">
|
||||
{template.category}
|
||||
</span>
|
||||
)}
|
||||
{template.description && (
|
||||
<p className="text-gray-300 text-sm mt-2">{template.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{onTemplateSelect && (
|
||||
<button
|
||||
onClick={() => onTemplateSelect(template.id)}
|
||||
className={`px-3 py-1 rounded text-xs transition-colors ${
|
||||
selectedTemplateId === template.id
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{selectedTemplateId === template.id ? 'Selected' : 'Select'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleEdit(template)}
|
||||
className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(template.id)}
|
||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-600 p-3 rounded text-gray-200 text-sm whitespace-pre-wrap">
|
||||
{template.templateText}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+92
-83
@@ -31,7 +31,7 @@ export default function Profile() {
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
setProfile(docSnap.data() as UserProfile);
|
||||
setNotificationsEnabled(docSnap.data().notificationsEnabled);
|
||||
setNotificationsEnabled(docSnap.data().notificationsEnabled ?? false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error);
|
||||
@@ -44,13 +44,6 @@ export default function Profile() {
|
||||
}
|
||||
}, [user, loadProfile]);
|
||||
|
||||
// Load notification settings
|
||||
useEffect(() => {
|
||||
if (profile.notificationsEnabled !== undefined) {
|
||||
setNotificationsEnabled(profile.notificationsEnabled);
|
||||
}
|
||||
}, [profile.notificationsEnabled]);
|
||||
|
||||
// Listen for FCM messages
|
||||
useEffect(() => {
|
||||
if (notificationsEnabled) {
|
||||
@@ -146,88 +139,104 @@ export default function Profile() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto mb-8 p-6 bg-gray-800 rounded-lg">
|
||||
<h2 className="text-2xl font-bold mb-6">Profile</h2>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2 text-lg">Avatar</label>
|
||||
{profile.avatarUrl && (
|
||||
<Image src={profile.avatarUrl} alt="Avatar" width={80} height={80} className="w-20 h-20 rounded-full mb-2" />
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
disabled={uploading}
|
||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded"
|
||||
/>
|
||||
{uploading && <p className="text-sm text-gray-400 mt-1">Uploading...</p>}
|
||||
<div className="max-w-2xl mx-auto p-6 space-y-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Profile Settings</h1>
|
||||
<p className="text-gray-400">Manage your account and preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Theme */}
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2 text-lg">Theme</label>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => handleThemeChange(e.target.value)}
|
||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded"
|
||||
>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2 text-lg">Notifications</label>
|
||||
<div className="flex items-center mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationsEnabled}
|
||||
onChange={(e) => handleNotificationToggle(e.target.checked)}
|
||||
className="w-6 h-6 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label className="ml-3 text-base font-medium">
|
||||
Enable push notifications for prayer reminders
|
||||
</label>
|
||||
{/* Avatar Section */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg shadow-lg">
|
||||
<h2 className="text-xl font-semibold mb-4 text-white">Profile Picture</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
{profile.avatarUrl ? (
|
||||
<Image src={profile.avatarUrl} alt="Avatar" width={80} height={80} className="w-20 h-20 rounded-full border-2 border-gray-600" />
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-full bg-gray-600 flex items-center justify-center text-gray-400">No Image</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
disabled={uploading}
|
||||
className="block w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700 disabled:opacity-50"
|
||||
/>
|
||||
{uploading && <p className="text-sm text-blue-400 mt-2">Uploading...</p>}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">
|
||||
Get notified about your prayer schedule and when prayers are answered.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2 text-lg">Prayer Categories</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
placeholder="Add category"
|
||||
className="flex-1 p-3 bg-gray-700 border border-gray-600 rounded"
|
||||
/>
|
||||
<button
|
||||
onClick={addCategory}
|
||||
className="px-4 py-3 bg-green-600 hover:bg-green-700 rounded"
|
||||
{/* Theme Section */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg shadow-lg">
|
||||
<h2 className="text-xl font-semibold mb-4 text-white">Appearance</h2>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Theme</label>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => handleThemeChange(e.target.value)}
|
||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(profile.categories || []).map(category => (
|
||||
<span key={category} className="bg-gray-700 px-3 py-1 rounded flex items-center gap-2">
|
||||
{category}
|
||||
<button
|
||||
onClick={() => removeCategory(category)}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Notifications Section */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg shadow-lg">
|
||||
<h2 className="text-xl font-semibold mb-4 text-white">Notifications</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationsEnabled}
|
||||
onChange={(e) => handleNotificationToggle(e.target.checked)}
|
||||
className="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<label className="ml-3 text-sm font-medium text-gray-300">
|
||||
Enable push notifications for prayer reminders
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">
|
||||
Get notified about your prayer schedule and when prayers are answered.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories Section */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg shadow-lg">
|
||||
<h2 className="text-xl font-semibold mb-4 text-white">Prayer Categories</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
placeholder="Add a new category"
|
||||
className="flex-1 p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
onClick={addCategory}
|
||||
className="px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(profile.categories || []).map(category => (
|
||||
<span key={category} className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-700 text-gray-300">
|
||||
{category}
|
||||
<button
|
||||
onClick={() => removeCategory(category)}
|
||||
className="ml-2 text-red-400 hover:text-red-300 transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Prayer {
|
||||
id: string;
|
||||
text: string;
|
||||
type: 'prayer' | 'praise';
|
||||
}
|
||||
|
||||
interface SharePrayerProps {
|
||||
prayer: Prayer;
|
||||
}
|
||||
|
||||
export default function SharePrayer({ prayer }: SharePrayerProps) {
|
||||
const shareText = `🙏 ${prayer.type === 'prayer' ? 'Prayer' : 'Praise'}: ${prayer.text.substring(0, 100)}${prayer.text.length > 100 ? '...' : ''}`;
|
||||
const shareUrl = window.location.href;
|
||||
|
||||
const shareToTwitter = () => {
|
||||
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`;
|
||||
window.open(url, '_blank', 'width=600,height=400');
|
||||
};
|
||||
|
||||
const shareToFacebook = () => {
|
||||
const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}"e=${encodeURIComponent(shareText)}`;
|
||||
window.open(url, '_blank', 'width=600,height=400');
|
||||
};
|
||||
|
||||
const shareToWhatsApp = () => {
|
||||
const url = `https://wa.me/?text=${encodeURIComponent(shareText + ' ' + shareUrl)}`;
|
||||
window.open(url, '_blank', 'width=600,height=400');
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${shareText} ${shareUrl}`);
|
||||
alert('Prayer copied to clipboard!');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<span className="text-xs text-gray-400">Share:</span>
|
||||
<button
|
||||
onClick={shareToTwitter}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm px-2 py-1 rounded hover:bg-blue-400/10 transition-colors"
|
||||
title="Share on Twitter"
|
||||
>
|
||||
🐦
|
||||
</button>
|
||||
<button
|
||||
onClick={shareToFacebook}
|
||||
className="text-blue-600 hover:text-blue-500 text-sm px-2 py-1 rounded hover:bg-blue-600/10 transition-colors"
|
||||
title="Share on Facebook"
|
||||
>
|
||||
📘
|
||||
</button>
|
||||
<button
|
||||
onClick={shareToWhatsApp}
|
||||
className="text-green-400 hover:text-green-300 text-sm px-2 py-1 rounded hover:bg-green-400/10 transition-colors"
|
||||
title="Share on WhatsApp"
|
||||
>
|
||||
💬
|
||||
</button>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="text-gray-400 hover:text-gray-300 text-sm px-2 py-1 rounded hover:bg-gray-400/10 transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,14 +39,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const unsubscribe = onAuthStateChanged(auth, async (user) => {
|
||||
setUser(user);
|
||||
if (user) {
|
||||
// Request FCM token and save to Firestore
|
||||
try {
|
||||
const token = await getToken(messaging, { vapidKey: 'YOUR_VAPID_KEY' }); // Replace with actual VAPID key
|
||||
if (token) {
|
||||
await setDoc(doc(db, 'users', user.uid), { fcmToken: token }, { merge: true });
|
||||
// Request FCM token and save to Firestore only if permission granted
|
||||
if (Notification.permission === 'granted') {
|
||||
try {
|
||||
const token = await getToken(messaging, { vapidKey: 'YOUR_VAPID_KEY' }); // Replace with actual VAPID key
|
||||
if (token) {
|
||||
await setDoc(doc(db, 'users', user.uid), { fcmToken: token }, { merge: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting FCM token:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting FCM token:', error);
|
||||
}
|
||||
|
||||
// Handle foreground messages
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
if (!admin.apps.length && process.env.FIREBASE_PROJECT_ID) {
|
||||
if (!admin.apps.length) {
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert({
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
|
||||
Reference in new issue
Block a user