- Add PrayerChain component for visualizing linked prayers - Add SharePrayer component with social media integration (Twitter, Facebook, WhatsApp, copy link) - Create /testimonies page for public answered prayer stories - Update Firestore rules for public testimony access - Enhance Organization tab with prayer chains - Add Testimonies navigation tab - Fix Firebase Admin SDK integration for server-side operations - Improve email system with proper server-side authentication - Update prayer form to handle optional fields correctly - Deploy updated Firestore security rules
244 lines
9.1 KiB
TypeScript
244 lines
9.1 KiB
TypeScript
'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>
|
||
);
|
||
} |