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:
nmemmert committed 2025-12-17 13:50:21 -05:00
1 parent 7214fdb89a
commit d9784738e4
32 files changed
+3146 -3236

No files matched your search

+288
View File
@@ -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>
);
}