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