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,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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user