Remove unused components from redesign - Admin, ClientLayout, EmailTemplatesManager, LoadingSkeleton, Navbar, PrayerChain, PrayerGoalsManager, PrayerTemplatesManager, Profile, SharePrayer

This commit is contained in:
nmemmert committed 2026-01-19 08:11:21 -05:00
1 parent b2cea9c0a3
commit 9fc41fffb9
11 files changed
-1928

No files matched your search

-1
View File
@@ -3,7 +3,6 @@
import { useState, useEffect } from 'react';
import { useAuth } from '@/context/AuthContext';
import Login from '@/components/Login';
import LoadingSkeleton from '@/components/LoadingSkeleton';
import BottomNav from '@/components/BottomNav';
import ReminderBanner from '@/components/ReminderBanner';
import DailyEntry from '@/components/DailyEntry';
-422
View File
@@ -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&apos;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>
);
}
-55
View File
@@ -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>
);
}
-288
View File
@@ -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>
);
}
-9
View File
@@ -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>
);
}
-207
View File
@@ -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>
);
}
-54
View File
@@ -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>
);
}
-316
View File
@@ -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>
);
}
-258
View File
@@ -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>
);
}
-244
View File
@@ -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>
);
}
-74
View File
@@ -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)}&quote=${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>
);
}