Files
prayer-app/src/components/Profile.tsx
T
nmemmert d9784738e4 feat: Add prayer chains, social sharing, and testimony wall
- Add PrayerChain component for visualizing linked prayers
- Add SharePrayer component with social media integration (Twitter, Facebook, WhatsApp, copy link)
- Create /testimonies page for public answered prayer stories
- Update Firestore rules for public testimony access
- Enhance Organization tab with prayer chains
- Add Testimonies navigation tab
- Fix Firebase Admin SDK integration for server-side operations
- Improve email system with proper server-side authentication
- Update prayer form to handle optional fields correctly
- Deploy updated Firestore security rules
2025-12-17 13:50:21 -05:00

244 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}