Remove unused components from redesign - Admin, ClientLayout, EmailTemplatesManager, LoadingSkeleton, Navbar, PrayerChain, PrayerGoalsManager, PrayerTemplatesManager, Profile, SharePrayer
This commit is contained in:
1 parent
b2cea9c0a3
commit
9fc41fffb9
11 files changed
-1928
No files matched your search
@@ -3,7 +3,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import Login from '@/components/Login';
|
import Login from '@/components/Login';
|
||||||
import LoadingSkeleton from '@/components/LoadingSkeleton';
|
|
||||||
import BottomNav from '@/components/BottomNav';
|
import BottomNav from '@/components/BottomNav';
|
||||||
import ReminderBanner from '@/components/ReminderBanner';
|
import ReminderBanner from '@/components/ReminderBanner';
|
||||||
import DailyEntry from '@/components/DailyEntry';
|
import DailyEntry from '@/components/DailyEntry';
|
||||||
|
|||||||
@@ -1,422 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { doc, getDoc, setDoc, collection, getDocs, deleteDoc } from 'firebase/firestore';
|
|
||||||
import { db } from '@/lib/firebase';
|
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
|
|
||||||
interface SMTPConfig {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
secure: boolean;
|
|
||||||
user: string;
|
|
||||||
pass: string;
|
|
||||||
from: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdminConfig {
|
|
||||||
adminEmails: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Admin() {
|
|
||||||
const { user, isAdmin } = useAuth();
|
|
||||||
const [config, setConfig] = useState<SMTPConfig>({
|
|
||||||
host: '',
|
|
||||||
port: 587,
|
|
||||||
secure: false,
|
|
||||||
user: '',
|
|
||||||
pass: '',
|
|
||||||
from: '',
|
|
||||||
});
|
|
||||||
const [adminConfig, setAdminConfig] = useState<AdminConfig>({
|
|
||||||
adminEmails: [],
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [message, setMessage] = useState('');
|
|
||||||
const [newAdminEmail, setNewAdminEmail] = useState('');
|
|
||||||
const [metrics, setMetrics] = useState({
|
|
||||||
totalUsers: 0,
|
|
||||||
totalPrayers: 0,
|
|
||||||
totalSharedPrayers: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadConfig = async () => {
|
|
||||||
try {
|
|
||||||
// Load SMTP config
|
|
||||||
const smtpDoc = await getDoc(doc(db, 'config', 'smtp'));
|
|
||||||
if (smtpDoc.exists()) {
|
|
||||||
setConfig(smtpDoc.data() as SMTPConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load admin config
|
|
||||||
const adminDoc = await getDoc(doc(db, 'config', 'admin'));
|
|
||||||
if (adminDoc.exists()) {
|
|
||||||
setAdminConfig(adminDoc.data() as AdminConfig);
|
|
||||||
} else {
|
|
||||||
// Default admin config
|
|
||||||
setAdminConfig({ adminEmails: ['your-admin-email@example.com'] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load metrics
|
|
||||||
const usersSnapshot = await getDocs(collection(db, 'users'));
|
|
||||||
const prayersSnapshot = await getDocs(collection(db, 'prayers'));
|
|
||||||
const sharedPrayersSnapshot = await getDocs(collection(db, 'sharedPrayers'));
|
|
||||||
|
|
||||||
setMetrics({
|
|
||||||
totalUsers: usersSnapshot.size,
|
|
||||||
totalPrayers: prayersSnapshot.size,
|
|
||||||
totalSharedPrayers: sharedPrayersSnapshot.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading config:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
|
||||||
// Validate required fields
|
|
||||||
if (!config.host.trim()) {
|
|
||||||
setMessage('SMTP host is required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!config.user.trim()) {
|
|
||||||
setMessage('SMTP username is required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!config.pass.trim()) {
|
|
||||||
setMessage('SMTP password is required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!config.from.trim()) {
|
|
||||||
setMessage('From email is required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isNaN(config.port) || config.port < 1 || config.port > 65535) {
|
|
||||||
setMessage('Port must be a valid number between 1 and 65535.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setMessage('');
|
|
||||||
try {
|
|
||||||
await setDoc(doc(db, 'config', 'smtp'), config);
|
|
||||||
setMessage('SMTP configuration saved successfully!');
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Error saving config:', error);
|
|
||||||
const err = error as { code?: string; message?: string };
|
|
||||||
if (err.code === 'permission-denied') {
|
|
||||||
setMessage('Permission denied. Please check your authentication and Firestore security rules.');
|
|
||||||
} else if (err.code === 'unavailable') {
|
|
||||||
setMessage('Firestore is currently unavailable. Please try again later.');
|
|
||||||
} else {
|
|
||||||
setMessage(`Error saving configuration: ${err.message || 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveAdminConfig = async () => {
|
|
||||||
setSaving(true);
|
|
||||||
setMessage('');
|
|
||||||
try {
|
|
||||||
await setDoc(doc(db, 'config', 'admin'), adminConfig);
|
|
||||||
setMessage('Admin configuration saved successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving admin config:', error);
|
|
||||||
setMessage('Error saving admin configuration.');
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAdminEmail = async () => {
|
|
||||||
if (!newAdminEmail.trim()) return;
|
|
||||||
|
|
||||||
const updatedEmails = [...adminConfig.adminEmails, newAdminEmail.trim()];
|
|
||||||
const updatedConfig = { ...adminConfig, adminEmails: updatedEmails };
|
|
||||||
|
|
||||||
try {
|
|
||||||
await setDoc(doc(db, 'config', 'admin'), updatedConfig);
|
|
||||||
setAdminConfig(updatedConfig);
|
|
||||||
setNewAdminEmail('');
|
|
||||||
setMessage('Admin email added successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding admin email:', error);
|
|
||||||
setMessage('Error adding admin email.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeAdminEmail = async (email: string) => {
|
|
||||||
const updatedEmails = adminConfig.adminEmails.filter(e => e !== email);
|
|
||||||
const updatedConfig = { ...adminConfig, adminEmails: updatedEmails };
|
|
||||||
|
|
||||||
try {
|
|
||||||
await setDoc(doc(db, 'config', 'admin'), updatedConfig);
|
|
||||||
setAdminConfig(updatedConfig);
|
|
||||||
setMessage('Admin email removed successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error removing admin email:', error);
|
|
||||||
setMessage('Error removing admin email.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCommunityPrayers = async () => {
|
|
||||||
if (!confirm('Are you sure you want to delete ALL community prayers? This action cannot be undone.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setMessage('');
|
|
||||||
try {
|
|
||||||
const sharedRef = collection(db, 'sharedPrayers');
|
|
||||||
const querySnapshot = await getDocs(sharedRef);
|
|
||||||
|
|
||||||
const deletePromises = querySnapshot.docs.map(doc => deleteDoc(doc.ref));
|
|
||||||
await Promise.all(deletePromises);
|
|
||||||
|
|
||||||
// Reload metrics to update the count
|
|
||||||
await loadConfig();
|
|
||||||
|
|
||||||
setMessage(`Successfully deleted ${querySnapshot.docs.length} community prayers.`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error clearing community prayers:', error);
|
|
||||||
setMessage('Error clearing community prayers.');
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
return (
|
|
||||||
<div className="max-w-2xl mx-auto p-6 bg-red-900 border border-red-700 rounded-lg">
|
|
||||||
<h2 className="text-xl font-bold text-red-100 mb-2">Access Denied</h2>
|
|
||||||
<p className="text-red-200">You don't have permission to access the admin panel.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div className="p-4">Loading admin settings...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-2xl mx-auto p-6 bg-gray-800 rounded-lg">
|
|
||||||
<h2 className="text-2xl font-bold mb-6 text-white">Admin Settings</h2>
|
|
||||||
|
|
||||||
<div className="mb-6 p-4 bg-gray-700 rounded">
|
|
||||||
<h3 className="text-xl font-semibold text-white mb-4">Analytics Dashboard</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-2xl font-bold text-blue-400">{metrics.totalUsers}</div>
|
|
||||||
<div className="text-sm text-gray-300">Total Users</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-2xl font-bold text-green-400">{metrics.totalPrayers}</div>
|
|
||||||
<div className="text-sm text-gray-300">Total Prayers</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-2xl font-bold text-purple-400">{metrics.totalSharedPrayers}</div>
|
|
||||||
<div className="text-sm text-gray-300">Shared Prayers</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xl font-semibold text-white mb-4">SMTP Configuration</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">SMTP Host</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={config.host}
|
|
||||||
onChange={(e) => setConfig(prev => ({ ...prev, host: e.target.value }))}
|
|
||||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
placeholder="smtp.gmail.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">Port</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={config.port}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseInt(e.target.value);
|
|
||||||
setConfig(prev => ({ ...prev, port: isNaN(value) ? 587 : value }));
|
|
||||||
}}
|
|
||||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">Username</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={config.user}
|
|
||||||
onChange={(e) => setConfig(prev => ({ ...prev, user: e.target.value }))}
|
|
||||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
placeholder="your-email@gmail.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={config.pass}
|
|
||||||
onChange={(e) => setConfig(prev => ({ ...prev, pass: e.target.value }))}
|
|
||||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
placeholder="your-app-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">From Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={config.from}
|
|
||||||
onChange={(e) => setConfig(prev => ({ ...prev, from: e.target.value }))}
|
|
||||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
placeholder="noreply@yourapp.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="secure"
|
|
||||||
checked={config.secure}
|
|
||||||
onChange={(e) => setConfig(prev => ({ ...prev, secure: e.target.checked }))}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<label htmlFor="secure" className="text-sm font-medium text-gray-300">Use SSL/TLS</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
<button
|
|
||||||
onClick={saveConfig}
|
|
||||||
disabled={saving}
|
|
||||||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded text-white font-medium"
|
|
||||||
>
|
|
||||||
{saving ? 'Saving...' : 'Save SMTP Settings'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<p className={`mt-2 text-sm ${message.includes('Error') ? 'text-red-400' : 'text-green-400'}`}>
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold text-white mb-4">Admin Configuration</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">Admin Emails</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={adminConfig.adminEmails.join(', ')}
|
|
||||||
onChange={(e) => setAdminConfig(prev => ({ ...prev, adminEmails: e.target.value.split(',').map(email => email.trim()) }))}
|
|
||||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
placeholder="admin1@example.com, admin2@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newAdminEmail}
|
|
||||||
onChange={(e) => setNewAdminEmail(e.target.value)}
|
|
||||||
className="flex-1 p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
placeholder="new-admin@example.com"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={addAdminEmail}
|
|
||||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-white font-medium"
|
|
||||||
>
|
|
||||||
Add Email
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
<button
|
|
||||||
onClick={saveAdminConfig}
|
|
||||||
disabled={saving}
|
|
||||||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded text-white font-medium"
|
|
||||||
>
|
|
||||||
{saving ? 'Saving...' : 'Save Admin Settings'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-600 pt-6 mt-6">
|
|
||||||
<h3 className="text-xl font-semibold text-white mb-4">Admin Email Management</h3>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">Current Admin Emails</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{adminConfig.adminEmails.map((email, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between bg-gray-700 p-2 rounded">
|
|
||||||
<span className="text-gray-200">{email}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => removeAdminEmail(email)}
|
|
||||||
className="text-red-400 hover:text-red-300"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={newAdminEmail}
|
|
||||||
onChange={(e) => setNewAdminEmail(e.target.value)}
|
|
||||||
placeholder="new-admin@example.com"
|
|
||||||
className="flex-1 p-2 bg-gray-700 border border-gray-600 rounded text-white"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={addAdminEmail}
|
|
||||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-white"
|
|
||||||
>
|
|
||||||
Add Admin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xl font-semibold text-white mb-4">Community Management</h3>
|
|
||||||
|
|
||||||
<div className="bg-gray-800 p-4 rounded-lg">
|
|
||||||
<h4 className="text-lg font-medium text-white mb-2">Clear Community Prayers</h4>
|
|
||||||
<p className="text-gray-300 mb-4">
|
|
||||||
Remove all prayers that have been shared to the community. This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={clearCommunityPrayers}
|
|
||||||
disabled={saving}
|
|
||||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed rounded text-white font-medium transition-colors duration-200"
|
|
||||||
>
|
|
||||||
{saving ? 'Clearing...' : 'Clear All Community Prayers'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { ThemeProvider } from 'next-themes';
|
|
||||||
import { AuthProvider } from '@/context/AuthContext';
|
|
||||||
import { Toaster } from 'react-hot-toast';
|
|
||||||
|
|
||||||
export default function ClientLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize theme from localStorage on mount
|
|
||||||
const stored = localStorage.getItem('theme-mode');
|
|
||||||
const html = document.documentElement;
|
|
||||||
|
|
||||||
if (stored === 'light') {
|
|
||||||
html.classList.remove('dark');
|
|
||||||
html.style.colorScheme = 'light';
|
|
||||||
} else if (stored === 'system') {
|
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
if (prefersDark) {
|
|
||||||
html.classList.add('dark');
|
|
||||||
html.style.colorScheme = 'dark';
|
|
||||||
} else {
|
|
||||||
html.classList.remove('dark');
|
|
||||||
html.style.colorScheme = 'light';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default to dark
|
|
||||||
html.classList.add('dark');
|
|
||||||
html.style.colorScheme = 'dark';
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
|
||||||
<AuthProvider>
|
|
||||||
{children}
|
|
||||||
<Toaster
|
|
||||||
position="top-right"
|
|
||||||
toastOptions={{
|
|
||||||
duration: 4000,
|
|
||||||
style: {
|
|
||||||
background: '#1f2937',
|
|
||||||
color: '#fff',
|
|
||||||
border: '1px solid #374151',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AuthProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { storage, db, requestNotificationPermission, onMessageListener } from '@/lib/firebase';
|
|
||||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
|
||||||
import { doc, getDoc, updateDoc } from 'firebase/firestore';
|
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
interface UserProfile {
|
|
||||||
avatarUrl?: string;
|
|
||||||
theme?: string;
|
|
||||||
categories?: string[];
|
|
||||||
notificationsEnabled?: boolean;
|
|
||||||
fcmToken?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Profile() {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { theme, setTheme } = useTheme();
|
|
||||||
const [profile, setProfile] = useState<UserProfile>({});
|
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
const [newCategory, setNewCategory] = useState('');
|
|
||||||
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
|
|
||||||
|
|
||||||
const loadProfile = useCallback(async () => {
|
|
||||||
if (!user) return;
|
|
||||||
try {
|
|
||||||
const docRef = doc(db, 'users', user.uid);
|
|
||||||
const docSnap = await getDoc(docRef);
|
|
||||||
if (docSnap.exists()) {
|
|
||||||
setProfile(docSnap.data() as UserProfile);
|
|
||||||
setNotificationsEnabled(docSnap.data().notificationsEnabled ?? false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading profile:', error);
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
loadProfile();
|
|
||||||
}
|
|
||||||
}, [user, loadProfile]);
|
|
||||||
|
|
||||||
// Listen for FCM messages
|
|
||||||
useEffect(() => {
|
|
||||||
if (notificationsEnabled) {
|
|
||||||
onMessageListener().then((payload: any) => {
|
|
||||||
console.log('Message received:', payload);
|
|
||||||
// Handle incoming messages (e.g., show notification)
|
|
||||||
new Notification(payload.notification.title, {
|
|
||||||
body: payload.notification.body,
|
|
||||||
icon: '/icon-192x192.png'
|
|
||||||
});
|
|
||||||
}).catch(err => console.log('Failed to listen for messages:', err));
|
|
||||||
}
|
|
||||||
}, [notificationsEnabled]);
|
|
||||||
|
|
||||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file || !user) return;
|
|
||||||
|
|
||||||
setUploading(true);
|
|
||||||
try {
|
|
||||||
const storageRef = ref(storage, `avatars/${user.uid}`);
|
|
||||||
await uploadBytes(storageRef, file);
|
|
||||||
const url = await getDownloadURL(storageRef);
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), { avatarUrl: url });
|
|
||||||
setProfile(prev => ({ ...prev, avatarUrl: url }));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading avatar:', error);
|
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleThemeChange = async (newTheme: string) => {
|
|
||||||
setTheme(newTheme);
|
|
||||||
if (user) {
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), { theme: newTheme });
|
|
||||||
setProfile(prev => ({ ...prev, theme: newTheme }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNotificationToggle = async (enabled: boolean) => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (enabled) {
|
|
||||||
const token = await requestNotificationPermission();
|
|
||||||
if (token) {
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), {
|
|
||||||
notificationsEnabled: true,
|
|
||||||
fcmToken: token
|
|
||||||
});
|
|
||||||
setProfile(prev => ({ ...prev, notificationsEnabled: true, fcmToken: token }));
|
|
||||||
setNotificationsEnabled(true);
|
|
||||||
} else {
|
|
||||||
alert('Notification permission denied. Please enable notifications in your browser settings.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), {
|
|
||||||
notificationsEnabled: false,
|
|
||||||
fcmToken: null
|
|
||||||
});
|
|
||||||
setProfile(prev => ({ ...prev, notificationsEnabled: false, fcmToken: undefined }));
|
|
||||||
setNotificationsEnabled(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating notification settings:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addCategory = async () => {
|
|
||||||
if (!newCategory.trim() || !user) return;
|
|
||||||
const updatedCategories = [...(profile.categories || []), newCategory.trim()];
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), { categories: updatedCategories });
|
|
||||||
setProfile(prev => ({ ...prev, categories: updatedCategories }));
|
|
||||||
setNewCategory('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeCategory = async (category: string) => {
|
|
||||||
if (!user) return;
|
|
||||||
const updatedCategories = (profile.categories || []).filter(c => c !== category);
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), { categories: updatedCategories });
|
|
||||||
setProfile(prev => ({ ...prev, categories: updatedCategories }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleNotifications = async () => {
|
|
||||||
if (!user) return;
|
|
||||||
const newStatus = !notificationsEnabled;
|
|
||||||
setNotificationsEnabled(newStatus);
|
|
||||||
await updateDoc(doc(db, 'users', user.uid), { notificationsEnabled: newStatus });
|
|
||||||
if (newStatus) {
|
|
||||||
requestNotificationPermission();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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"
|
|
||||||
>
|
|
||||||
<option value="dark">Dark</option>
|
|
||||||
<option value="light">Light</option>
|
|
||||||
<option value="system">System</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
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