Major improvements: Firebase data migration, enhanced loading screens, multi-day prayer tracking
- Migrated all localStorage data to Firebase for cross-device sync
- Prayer drafts now stored in users/{userId}/drafts
- Reminder settings in users/{userId}/settings
- Prayer statistics in users/{userId}/statistics
- Implemented persistent 'most prayed for' tracking in Firebase
- Enhanced loading screen with animated backgrounds and gradients
- Fixed auth session restoration on hard refresh
- Added prayedDates array for multi-day prayer tracking
- Calendar now accurately shows all days a prayer was prayed for
- Enabled Firestore offline persistence for offline-first experience
- Updated Firestore security rules for new data structure
- Improved clock display (removed leading zero from hours)
- Moved clock below date on homepage with appropriate sizing
- Fixed data loading race conditions between auth and Firestore
This commit is contained in:
1 parent
2c29fc28d6
commit
67c5ff1b35
13 files changed
+419
-138
No files matched your search
@@ -4,6 +4,21 @@ service cloud.firestore {
|
|||||||
// Allow authenticated users to read/write their own data
|
// Allow authenticated users to read/write their own data
|
||||||
match /users/{userId} {
|
match /users/{userId} {
|
||||||
allow read, write: if request.auth != null && request.auth.uid == userId;
|
allow read, write: if request.auth != null && request.auth.uid == userId;
|
||||||
|
|
||||||
|
// Allow users to read/write their own statistics
|
||||||
|
match /statistics/{document=**} {
|
||||||
|
allow read, write: if request.auth != null && request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow users to read/write their own settings
|
||||||
|
match /settings/{document=**} {
|
||||||
|
allow read, write: if request.auth != null && request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow users to read/write their own drafts
|
||||||
|
match /drafts/{document=**} {
|
||||||
|
allow read, write: if request.auth != null && request.auth.uid == userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow public reading of answered prayers for testimony wall
|
// Allow public reading of answered prayers for testimony wall
|
||||||
|
|||||||
+185
-40
@@ -13,7 +13,7 @@ import AboutPage from '@/components/AboutPage';
|
|||||||
import HomePage from '@/components/HomePage';
|
import HomePage from '@/components/HomePage';
|
||||||
import AnsweredPrayers from '@/components/AnsweredPrayers';
|
import AnsweredPrayers from '@/components/AnsweredPrayers';
|
||||||
import { db } from '@/lib/firebase';
|
import { db } from '@/lib/firebase';
|
||||||
import { collection, addDoc, getDocs, updateDoc, query, where, doc } from 'firebase/firestore';
|
import { collection, addDoc, getDocs, updateDoc, query, where, doc, setDoc, getDoc } from 'firebase/firestore';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -26,13 +26,16 @@ interface Prayer {
|
|||||||
answered?: boolean;
|
answered?: boolean;
|
||||||
answerDetails?: string;
|
answerDetails?: string;
|
||||||
prayFrequency?: number; // 1-7 times per week
|
prayFrequency?: number; // 1-7 times per week
|
||||||
lastPrayedFor?: string; // ISO date string
|
lastPrayedFor?: string; // ISO date string - kept for backward compatibility
|
||||||
|
prayedDates?: string[]; // Array of dates when prayed for
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [activeTab, setActiveTab] = useState<'home' | 'compose' | 'journal' | 'answered' | 'praying' | 'reflections' | 'about'>('home');
|
const [activeTab, setActiveTab] = useState<'home' | 'compose' | 'journal' | 'answered' | 'praying' | 'reflections' | 'about'>('home');
|
||||||
const [prayers, setPrayers] = useState<Prayer[]>([]);
|
const [prayers, setPrayers] = useState<Prayer[]>([]);
|
||||||
const [editingPrayer, setEditingPrayer] = useState<Prayer | null>(null);
|
const [editingPrayer, setEditingPrayer] = useState<Prayer | null>(null);
|
||||||
|
const [prayersLoading, setPrayersLoading] = useState(false);
|
||||||
|
const [mostPrayedFor, setMostPrayedFor] = useState<string>('');
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
|
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
@@ -45,8 +48,13 @@ export default function Home() {
|
|||||||
|
|
||||||
// Load prayers from Firestore
|
// Load prayers from Firestore
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) {
|
||||||
|
setPrayers([]);
|
||||||
|
setPrayersLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrayersLoading(true);
|
||||||
const loadPrayers = async () => {
|
const loadPrayers = async () => {
|
||||||
try {
|
try {
|
||||||
const prayersRef = collection(db, 'prayers');
|
const prayersRef = collection(db, 'prayers');
|
||||||
@@ -62,31 +70,61 @@ export default function Home() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading prayers:', error);
|
console.error('Error loading prayers:', error);
|
||||||
toast.error('Failed to load prayers');
|
toast.error('Failed to load prayers');
|
||||||
|
} finally {
|
||||||
|
setPrayersLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPrayers();
|
loadPrayers();
|
||||||
|
fetchPrayerStatistics();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
// Load draft from localStorage
|
// Load draft from Firebase
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const draft = localStorage.getItem('prayerDraft');
|
if (!user) return;
|
||||||
if (draft) {
|
|
||||||
try {
|
|
||||||
const { date: draftDate, text: draftText, prayFor: draftPrayFor } = JSON.parse(draft);
|
|
||||||
setDate(draftDate || new Date().toISOString().split('T')[0]);
|
|
||||||
setText(draftText || '');
|
|
||||||
setPrayFor(draftPrayFor || '');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading draft:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Save draft to localStorage
|
const loadDraft = async () => {
|
||||||
|
try {
|
||||||
|
const draftRef = doc(db, 'users', user.uid, 'drafts', 'currentPrayer');
|
||||||
|
const draftSnap = await getDoc(draftRef);
|
||||||
|
|
||||||
|
if (draftSnap.exists()) {
|
||||||
|
const draftData = draftSnap.data();
|
||||||
|
setDate(draftData.date || new Date().toISOString().split('T')[0]);
|
||||||
|
setText(draftData.text || '');
|
||||||
|
setPrayFor(draftData.prayFor || '');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Silently fail if offline
|
||||||
|
if (error?.code !== 'unavailable') {
|
||||||
|
console.error('Error loading draft:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDraft();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// Save draft to Firebase
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('prayerDraft', JSON.stringify({ date, text, prayFor }));
|
if (!user) return;
|
||||||
}, [date, text, prayFor]);
|
|
||||||
|
const saveDraft = async () => {
|
||||||
|
try {
|
||||||
|
const draftRef = doc(db, 'users', user.uid, 'drafts', 'currentPrayer');
|
||||||
|
await setDoc(draftRef, { date, text, prayFor, updatedAt: new Date().toISOString() }, { merge: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
// Silently fail if offline - will sync when connection is restored
|
||||||
|
if (error?.code !== 'unavailable') {
|
||||||
|
console.error('Error saving draft:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounce the save to avoid too many writes
|
||||||
|
const timeoutId = setTimeout(saveDraft, 1000);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [date, text, prayFor, user]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -137,7 +175,12 @@ export default function Home() {
|
|||||||
setDate(new Date().toISOString().split('T')[0]);
|
setDate(new Date().toISOString().split('T')[0]);
|
||||||
setText('');
|
setText('');
|
||||||
setPrayFor('');
|
setPrayFor('');
|
||||||
localStorage.removeItem('prayerDraft');
|
|
||||||
|
// Clear draft from Firebase
|
||||||
|
if (user) {
|
||||||
|
const draftRef = doc(db, 'users', user.uid, 'drafts', 'currentPrayer');
|
||||||
|
await setDoc(draftRef, { date: new Date().toISOString().split('T')[0], text: '', prayFor: '' });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving prayer:', error);
|
console.error('Error saving prayer:', error);
|
||||||
toast.error('Failed to save prayer');
|
toast.error('Failed to save prayer');
|
||||||
@@ -164,17 +207,86 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePrayerStatistics = async (prayFor: string) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statsDocRef = doc(db, 'users', user.uid, 'statistics', 'prayerStats');
|
||||||
|
|
||||||
|
// Get current stats
|
||||||
|
let stats: Record<string, number> = {};
|
||||||
|
try {
|
||||||
|
const statsSnap = await getDoc(statsDocRef);
|
||||||
|
if (statsSnap.exists()) {
|
||||||
|
stats = statsSnap.data() as Record<string, number>;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Stats don't exist yet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment count for this prayFor item
|
||||||
|
stats[prayFor] = (stats[prayFor] || 0) + 1;
|
||||||
|
|
||||||
|
// Update the stats document
|
||||||
|
await setDoc(statsDocRef, stats, { merge: true });
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
const sorted = Object.entries(stats).sort((a, b) => b[1] - a[1]);
|
||||||
|
setMostPrayedFor(sorted.length > 0 ? sorted[0][0] : '');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating statistics:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPrayerStatistics = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statsDocRef = doc(db, 'users', user.uid, 'statistics', 'prayerStats');
|
||||||
|
const statsSnap = await getDoc(statsDocRef);
|
||||||
|
|
||||||
|
if (statsSnap.exists()) {
|
||||||
|
const stats = statsSnap.data() as Record<string, number>;
|
||||||
|
const sorted = Object.entries(stats).sort((a, b) => b[1] - a[1]);
|
||||||
|
setMostPrayedFor(sorted[0][0]);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Silently fail if offline - will retry when connection is restored
|
||||||
|
if (error?.code !== 'unavailable') {
|
||||||
|
console.error('Error fetching statistics:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMarkPrayed = async (prayerId: string) => {
|
const handleMarkPrayed = async (prayerId: string) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const prayer = prayers.find(p => p.id === prayerId);
|
||||||
|
|
||||||
|
if (!prayer) return;
|
||||||
|
|
||||||
|
// Get existing prayed dates or initialize array
|
||||||
|
const existingDates = prayer.prayedDates || [];
|
||||||
|
|
||||||
|
// Add today's date if not already in the array
|
||||||
|
const updatedDates = existingDates.includes(today)
|
||||||
|
? existingDates
|
||||||
|
: [...existingDates, today];
|
||||||
|
|
||||||
await updateDoc(doc(db, 'prayers', prayerId), {
|
await updateDoc(doc(db, 'prayers', prayerId), {
|
||||||
lastPrayedFor: today,
|
lastPrayedFor: today,
|
||||||
|
prayedDates: updatedDates,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update statistics if this prayer has a prayFor value
|
||||||
|
if (prayer.prayFor) {
|
||||||
|
await updatePrayerStatistics(prayer.prayFor);
|
||||||
|
}
|
||||||
|
|
||||||
setPrayers(prev =>
|
setPrayers(prev =>
|
||||||
prev.map(p => (p.id === prayerId ? { ...p, lastPrayedFor: today } : p))
|
prev.map(p => (p.id === prayerId ? { ...p, lastPrayedFor: today, prayedDates: updatedDates } : p))
|
||||||
);
|
);
|
||||||
toast.success('Prayed!');
|
toast.success('Prayed!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -183,38 +295,70 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (authLoading) {
|
if (authLoading || prayersLoading) {
|
||||||
|
const loadingMessage = authLoading ? 'Preparing your sacred space...' : 'Loading your prayers...';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4 overflow-hidden">
|
||||||
<div className="text-center">
|
{/* Animated Background */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-pulse"></div>
|
||||||
|
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
|
||||||
|
<div className="absolute top-1/2 left-1/2 w-64 h-64 bg-pink-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '2s' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center relative z-10">
|
||||||
|
{/* Animated Icon */}
|
||||||
<div className="mb-8 relative">
|
<div className="mb-8 relative">
|
||||||
<div className="w-24 h-24 mx-auto relative">
|
<div className="w-32 h-32 mx-auto relative">
|
||||||
<div className="absolute inset-0 border-4 border-blue-500/30 rounded-full animate-spin"></div>
|
{/* Outer rotating ring */}
|
||||||
<div
|
<div className="absolute inset-0 border-4 border-blue-500/20 rounded-full"></div>
|
||||||
className="absolute inset-2 border-4 border-transparent border-t-blue-400 rounded-full animate-spin"
|
<div className="absolute inset-0 border-4 border-transparent border-t-blue-400 border-r-blue-400 rounded-full animate-spin"></div>
|
||||||
style={{ animationDirection: 'reverse', animationDuration: '1.5s' }}
|
|
||||||
|
{/* Middle rotating ring - reverse direction */}
|
||||||
|
<div className="absolute inset-3 border-4 border-purple-500/20 rounded-full"></div>
|
||||||
|
<div
|
||||||
|
className="absolute inset-3 border-4 border-transparent border-b-purple-400 border-l-purple-400 rounded-full animate-spin"
|
||||||
|
style={{ animationDirection: 'reverse', animationDuration: '2s' }}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
{/* Inner pulsing circle */}
|
||||||
|
<div className="absolute inset-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 rounded-full animate-pulse"></div>
|
||||||
|
|
||||||
|
{/* App Icon */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
|
<img
|
||||||
|
src="/icon-192x192.png"
|
||||||
|
alt="Haver icon"
|
||||||
|
className="w-16 h-16 rounded-lg shadow-2xl animate-pulse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
|
{/* App Title */}
|
||||||
<h1 className="text-2xl font-bold text-white mb-2 animate-pulse">Haver</h1>
|
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-3 animate-pulse">
|
||||||
<p className="text-gray-400 text-lg mb-4">Preparing your sacred space...</p>
|
Haver
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Loading Message */}
|
||||||
|
<p className="text-gray-300 text-lg mb-6 animate-pulse">
|
||||||
|
{loadingMessage}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="max-w-xs mx-auto">
|
{/* Inspirational Quote */}
|
||||||
<blockquote className="text-gray-300 italic text-sm leading-relaxed">
|
<div className="max-w-md mx-auto mb-8 px-4">
|
||||||
|
<blockquote className="text-gray-400 italic text-sm leading-relaxed">
|
||||||
"Be anxious for nothing, but in everything by prayer and supplication, with thanksgiving, let your requests be made known to God."
|
"Be anxious for nothing, but in everything by prayer and supplication, with thanksgiving, let your requests be made known to God."
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<cite className="text-gray-500 text-xs mt-2 block">— Philippians 4:6</cite>
|
<cite className="text-gray-600 text-xs mt-2 block">— Philippians 4:6</cite>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center space-x-2 mt-8">
|
{/* Animated Dots */}
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{ animationDelay: '0s' }}></div>
|
<div className="flex justify-center space-x-3">
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
|
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
|
<div className="w-3 h-3 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||||
|
<div className="w-3 h-3 bg-pink-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,6 +381,7 @@ export default function Home() {
|
|||||||
onStartPraying={() => setActiveTab('compose')}
|
onStartPraying={() => setActiveTab('compose')}
|
||||||
onSelectPrayer={handleSelectPrayer}
|
onSelectPrayer={handleSelectPrayer}
|
||||||
userName={user.displayName || user.email || undefined}
|
userName={user.displayName || user.email || undefined}
|
||||||
|
mostPrayedFor={mostPrayedFor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import React, { useState } from 'react';
|
|||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { Info, Smartphone, BellRing } from 'lucide-react';
|
import { Info, Smartphone, BellRing } from 'lucide-react';
|
||||||
import { APP_VERSION, WHATS_NEW_BRIEF } from '@/lib/version';
|
import { APP_VERSION, WHATS_NEW_BRIEF } from '@/lib/version';
|
||||||
|
import { db } from '@/lib/firebase';
|
||||||
|
import { doc, getDoc, setDoc } from 'firebase/firestore';
|
||||||
|
|
||||||
interface AboutPageProps {
|
interface AboutPageProps {
|
||||||
userEmail?: string;
|
userEmail?: string;
|
||||||
@@ -55,49 +57,86 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
|
|||||||
setNotificationPermission(Notification.permission);
|
setNotificationPermission(Notification.permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load saved settings
|
// Load saved settings from Firebase
|
||||||
const savedEnabled = localStorage.getItem('prayerRemindersEnabled') === 'true';
|
const loadSettings = async () => {
|
||||||
const savedTime = localStorage.getItem('prayerReminderTime') || '09:00';
|
if (!user) return;
|
||||||
setNotificationsEnabled(savedEnabled);
|
|
||||||
setReminderTime(savedTime);
|
try {
|
||||||
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
// Schedule notification for supported platforms
|
const settingsSnap = await getDoc(settingsRef);
|
||||||
if (savedEnabled && webPushSupport) {
|
|
||||||
scheduleNotification();
|
if (settingsSnap.exists()) {
|
||||||
}
|
const settings = settingsSnap.data();
|
||||||
}, []);
|
setNotificationsEnabled(settings.enabled || false);
|
||||||
|
setReminderTime(settings.time || '09:00');
|
||||||
|
|
||||||
|
if (settings.enabled && webPushSupport) {
|
||||||
|
scheduleNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code !== 'unavailable') {
|
||||||
|
console.error('Error loading settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const requestNotificationPermission = async () => {
|
const requestNotificationPermission = async () => {
|
||||||
if (!supportsWebPush) return;
|
if (!supportsWebPush || !user) return;
|
||||||
|
|
||||||
if ('Notification' in window) {
|
if ('Notification' in window) {
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
setNotificationPermission(permission);
|
setNotificationPermission(permission);
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
setNotificationsEnabled(true);
|
setNotificationsEnabled(true);
|
||||||
localStorage.setItem('prayerRemindersEnabled', 'true');
|
|
||||||
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
|
// Save to Firebase
|
||||||
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
|
await setDoc(settingsRef, {
|
||||||
|
enabled: true,
|
||||||
|
time: reminderTime,
|
||||||
|
lastReminderDate: new Date().toISOString()
|
||||||
|
}, { merge: true });
|
||||||
|
|
||||||
scheduleNotification();
|
scheduleNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleNotifications = async () => {
|
const toggleNotifications = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
if (supportsWebPush) {
|
if (supportsWebPush) {
|
||||||
if (!notificationsEnabled && notificationPermission !== 'granted') {
|
if (!notificationsEnabled && notificationPermission !== 'granted') {
|
||||||
await requestNotificationPermission();
|
await requestNotificationPermission();
|
||||||
} else {
|
} else {
|
||||||
setNotificationsEnabled(!notificationsEnabled);
|
setNotificationsEnabled(!notificationsEnabled);
|
||||||
localStorage.setItem('prayerRemindersEnabled', String(!notificationsEnabled));
|
|
||||||
|
// Save to Firebase
|
||||||
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
|
await setDoc(settingsRef, {
|
||||||
|
enabled: !notificationsEnabled,
|
||||||
|
time: reminderTime,
|
||||||
|
lastReminderDate: new Date().toISOString()
|
||||||
|
}, { merge: true });
|
||||||
|
|
||||||
if (!notificationsEnabled) {
|
if (!notificationsEnabled) {
|
||||||
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
|
|
||||||
scheduleNotification();
|
scheduleNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For iOS and unsupported platforms, just enable in-app reminders
|
// For iOS and unsupported platforms, just enable in-app reminders
|
||||||
setNotificationsEnabled(!notificationsEnabled);
|
setNotificationsEnabled(!notificationsEnabled);
|
||||||
localStorage.setItem('prayerRemindersEnabled', String(!notificationsEnabled));
|
|
||||||
|
// Save to Firebase
|
||||||
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
|
await setDoc(settingsRef, {
|
||||||
|
enabled: !notificationsEnabled,
|
||||||
|
time: reminderTime
|
||||||
|
}, { merge: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,15 +152,24 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeChange = (newTime: string) => {
|
const handleTimeChange = (newTime: string) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
setReminderTime(newTime);
|
setReminderTime(newTime);
|
||||||
localStorage.setItem('prayerReminderTime', newTime);
|
|
||||||
|
// Save to Firebase
|
||||||
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
|
setDoc(settingsRef, {
|
||||||
|
enabled: notificationsEnabled,
|
||||||
|
time: newTime
|
||||||
|
}, { merge: true });
|
||||||
|
|
||||||
if (notificationsEnabled) {
|
if (notificationsEnabled) {
|
||||||
scheduleNotification();
|
scheduleNotification();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduleNotification = () => {
|
const scheduleNotification = () => {
|
||||||
if (!supportsWebPush || !notificationsEnabled) return;
|
if (!supportsWebPush || !notificationsEnabled || !user) return;
|
||||||
|
|
||||||
// Calculate time until next reminder
|
// Calculate time until next reminder
|
||||||
const [hours, minutes] = reminderTime.split(':').map(Number);
|
const [hours, minutes] = reminderTime.split(':').map(Number);
|
||||||
@@ -139,7 +187,7 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
|
|||||||
// Only schedule if we have a valid time difference
|
// Only schedule if we have a valid time difference
|
||||||
if (timeUntilNotification > 0) {
|
if (timeUntilNotification > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if ('Notification' in window && Notification.permission === 'granted' && notificationsEnabled) {
|
if ('Notification' in window && Notification.permission === 'granted' && notificationsEnabled && user) {
|
||||||
new Notification('Time to Pray', {
|
new Notification('Time to Pray', {
|
||||||
body: 'Take a moment to connect with God and pray.',
|
body: 'Take a moment to connect with God and pray.',
|
||||||
icon: '/icon-192x192.png',
|
icon: '/icon-192x192.png',
|
||||||
@@ -148,8 +196,11 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
|
|||||||
requireInteraction: false,
|
requireInteraction: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save reminder shown timestamp
|
// Save reminder shown timestamp to Firebase
|
||||||
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
|
setDoc(settingsRef, {
|
||||||
|
lastReminderDate: new Date().toISOString()
|
||||||
|
}, { merge: true });
|
||||||
|
|
||||||
// Reschedule for next day
|
// Reschedule for next day
|
||||||
setTimeout(scheduleNotification, 24 * 60 * 60 * 1000);
|
setTimeout(scheduleNotification, 24 * 60 * 60 * 1000);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface Prayer {
|
|||||||
answerDetails?: string;
|
answerDetails?: string;
|
||||||
prayFrequency?: number; // 1-7 times per week
|
prayFrequency?: number; // 1-7 times per week
|
||||||
lastPrayedFor?: string; // ISO date string
|
lastPrayedFor?: string; // ISO date string
|
||||||
|
prayedDates?: string[]; // Array of dates when prayed for
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnsweredPrayersProps {
|
interface AnsweredPrayersProps {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface Prayer {
|
|||||||
text: string;
|
text: string;
|
||||||
prayFor?: string;
|
prayFor?: string;
|
||||||
lastPrayedFor?: string;
|
lastPrayedFor?: string;
|
||||||
|
prayedDates?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CalendarViewProps {
|
interface CalendarViewProps {
|
||||||
@@ -24,13 +25,19 @@ export default function CalendarView({ prayers, onDayClick }: CalendarViewProps)
|
|||||||
}, [prayers]);
|
}, [prayers]);
|
||||||
|
|
||||||
const daysWithPrayedFor = useMemo(() => {
|
const daysWithPrayedFor = useMemo(() => {
|
||||||
const dates = new Set<string>();
|
const dateCounts = new Map<string, number>();
|
||||||
prayers.forEach(p => {
|
prayers.forEach(p => {
|
||||||
if (p.lastPrayedFor) {
|
// Check both old lastPrayedFor and new prayedDates array
|
||||||
dates.add(p.lastPrayedFor);
|
if (p.prayedDates && p.prayedDates.length > 0) {
|
||||||
|
p.prayedDates.forEach(date => {
|
||||||
|
dateCounts.set(date, (dateCounts.get(date) || 0) + 1);
|
||||||
|
});
|
||||||
|
} else if (p.lastPrayedFor) {
|
||||||
|
// Backward compatibility with old format
|
||||||
|
dateCounts.set(p.lastPrayedFor, (dateCounts.get(p.lastPrayedFor) || 0) + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return dates;
|
return dateCounts;
|
||||||
}, [prayers]);
|
}, [prayers]);
|
||||||
|
|
||||||
const getDaysInMonth = (date: Date) => {
|
const getDaysInMonth = (date: Date) => {
|
||||||
@@ -120,7 +127,7 @@ export default function CalendarView({ prayers, onDayClick }: CalendarViewProps)
|
|||||||
? prayers.filter(p => p.date === dateString).length
|
? prayers.filter(p => p.date === dateString).length
|
||||||
: 0;
|
: 0;
|
||||||
const prayedForCount = dateString
|
const prayedForCount = dateString
|
||||||
? prayers.filter(p => p.lastPrayedFor === dateString).length
|
? daysWithPrayedFor.get(dateString) || 0
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Determine color based on activity type
|
// Determine color based on activity type
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function Clock() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set initial time
|
// Set initial time
|
||||||
setTime(new Date().toLocaleTimeString('en-US', {
|
setTime(new Date().toLocaleTimeString('en-US', {
|
||||||
hour: '2-digit',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: true
|
hour12: true
|
||||||
@@ -17,7 +17,7 @@ export default function Clock() {
|
|||||||
// Update time every second
|
// Update time every second
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setTime(new Date().toLocaleTimeString('en-US', {
|
setTime(new Date().toLocaleTimeString('en-US', {
|
||||||
hour: '2-digit',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: true
|
hour12: true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { PenSquare, Calendar as CalendarIcon, HandHeart, CheckCircle2, Sparkles, Save, Loader2 } from 'lucide-react';
|
import { PenSquare, Calendar as CalendarIcon, HandHeart, CheckCircle2, Sparkles, Save, Loader2, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
interface DailyEntryProps {
|
interface DailyEntryProps {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -85,14 +85,19 @@ export default function DailyEntry({
|
|||||||
{/* Pray For */}
|
{/* Pray For */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-300 mb-3 inline-flex items-center gap-1.5"><HandHeart className="w-4 h-4" /> Praying For (Optional)</label>
|
<label className="block text-sm font-semibold text-slate-300 mb-3 inline-flex items-center gap-1.5"><HandHeart className="w-4 h-4" /> Praying For (Optional)</label>
|
||||||
<input
|
<div className="relative">
|
||||||
type="text"
|
<input
|
||||||
value={prayFor}
|
type="text"
|
||||||
onChange={(e) => onPrayForChange(e.target.value)}
|
value={prayFor}
|
||||||
placeholder="Person, situation, or concern..."
|
onChange={(e) => onPrayForChange(e.target.value)}
|
||||||
list="prayfor-options"
|
placeholder="Person, situation, or concern..."
|
||||||
className="w-full px-4 py-3 rounded-xl bg-slate-800/60 backdrop-blur-xl border border-slate-700/50 text-white placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
list="prayfor-options"
|
||||||
/>
|
className="w-full px-4 py-3.5 pr-12 rounded-xl bg-slate-800/60 backdrop-blur-xl border border-slate-700/50 text-white placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||||
|
/>
|
||||||
|
{prayForOptions.length > 0 && (
|
||||||
|
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-6 h-6 text-slate-400 pointer-events-none" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{prayForOptions.length > 0 && (
|
{prayForOptions.length > 0 && (
|
||||||
<datalist id="prayfor-options">
|
<datalist id="prayfor-options">
|
||||||
{prayForOptions.map((opt) => (
|
{prayForOptions.map((opt) => (
|
||||||
|
|||||||
+24
-22
@@ -14,6 +14,7 @@ interface Prayer {
|
|||||||
answerDetails?: string;
|
answerDetails?: string;
|
||||||
prayFrequency?: number; // 1-7 times per week
|
prayFrequency?: number; // 1-7 times per week
|
||||||
lastPrayedFor?: string; // ISO date string
|
lastPrayedFor?: string; // ISO date string
|
||||||
|
prayedDates?: string[]; // Array of dates when prayed for
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HomePageProps {
|
interface HomePageProps {
|
||||||
@@ -22,9 +23,10 @@ interface HomePageProps {
|
|||||||
onStartPraying: () => void;
|
onStartPraying: () => void;
|
||||||
onSelectPrayer: (prayer: Prayer) => void;
|
onSelectPrayer: (prayer: Prayer) => void;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
|
mostPrayedFor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HomePage({ prayers, todayCount, onStartPraying, onSelectPrayer, userName }: HomePageProps) {
|
export default function HomePage({ prayers, todayCount, onStartPraying, onSelectPrayer, userName, mostPrayedFor }: HomePageProps) {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const todayPrayers = prayers.filter(p => p.date === today);
|
const todayPrayers = prayers.filter(p => p.date === today);
|
||||||
const recentPrayers = prayers.slice(0, 5);
|
const recentPrayers = prayers.slice(0, 5);
|
||||||
@@ -55,7 +57,12 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
|
|||||||
const activePrayerCount = prayers.filter(p => !p.answered).length;
|
const activePrayerCount = prayers.filter(p => !p.answered).length;
|
||||||
|
|
||||||
// Count how many active prayers were prayed for today
|
// Count how many active prayers were prayed for today
|
||||||
const prayedTodayCount = prayers.filter(p => !p.answered && p.lastPrayedFor === today).length;
|
const prayedTodayCount = prayers.filter(p =>
|
||||||
|
!p.answered && (
|
||||||
|
p.prayedDates?.includes(today) ||
|
||||||
|
p.lastPrayedFor === today
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
// Calculate actual streak - consecutive days with prayers
|
// Calculate actual streak - consecutive days with prayers
|
||||||
const calculateStreak = () => {
|
const calculateStreak = () => {
|
||||||
@@ -65,8 +72,13 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
|
|||||||
const activityDates = new Set<string>();
|
const activityDates = new Set<string>();
|
||||||
prayers.forEach(p => {
|
prayers.forEach(p => {
|
||||||
activityDates.add(p.date); // Date prayer was created
|
activityDates.add(p.date); // Date prayer was created
|
||||||
if (p.lastPrayedFor) {
|
|
||||||
activityDates.add(p.lastPrayedFor); // Date marked as prayed for
|
// Add all prayed dates from the array
|
||||||
|
if (p.prayedDates && p.prayedDates.length > 0) {
|
||||||
|
p.prayedDates.forEach((date: string) => activityDates.add(date));
|
||||||
|
} else if (p.lastPrayedFor) {
|
||||||
|
// Backward compatibility
|
||||||
|
activityDates.add(p.lastPrayedFor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,15 +113,6 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
|
|||||||
? Math.round((answeredPrayers / totalPrayers) * 100)
|
? Math.round((answeredPrayers / totalPrayers) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Get most prayed for person/topic based on lastPrayedFor clicks
|
|
||||||
const prayerForCounts: Record<string, number> = {};
|
|
||||||
prayers.forEach(p => {
|
|
||||||
if (p.prayFor && p.lastPrayedFor) {
|
|
||||||
prayerForCounts[p.prayFor] = (prayerForCounts[p.prayFor] || 0) + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const mostPrayedFor = Object.entries(prayerForCounts).sort((a, b) => b[1] - a[1])[0];
|
|
||||||
|
|
||||||
// Recent answered prayers
|
// Recent answered prayers
|
||||||
const recentAnswered = prayers.filter(p => p.answered).slice(0, 3);
|
const recentAnswered = prayers.filter(p => p.answered).slice(0, 3);
|
||||||
|
|
||||||
@@ -122,15 +125,8 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
|
|||||||
<div className="absolute bottom-20 left-10 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl"></div>
|
<div className="absolute bottom-20 left-10 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clock */}
|
|
||||||
<div className="max-w-md mx-auto mb-12">
|
|
||||||
<div className="text-center">
|
|
||||||
<Clock />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header with Personalized Greeting */}
|
{/* Header with Personalized Greeting */}
|
||||||
<div className="max-w-md mx-auto mb-12 text-center">
|
<div className="max-w-md mx-auto mb-8 text-center">
|
||||||
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
|
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
|
||||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-3">
|
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent mb-3">
|
||||||
{getGreeting()}, {getFirstName()}!
|
{getGreeting()}, {getFirstName()}!
|
||||||
@@ -151,9 +147,15 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
|
|||||||
All caught up! Start a new prayer to keep your journey going.
|
All caught up! Start a new prayer to keep your journey going.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-slate-400 mt-2">
|
<p className="text-slate-400 mt-4 text-sm">
|
||||||
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
|
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
|
{/* Clock - Below Date */}
|
||||||
|
<div className="mt-3 flex justify-center">
|
||||||
|
<div className="scale-75">
|
||||||
|
<Clock />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface Prayer {
|
|||||||
answered?: boolean;
|
answered?: boolean;
|
||||||
prayFrequency?: number; // 1-7 times per week
|
prayFrequency?: number; // 1-7 times per week
|
||||||
lastPrayedFor?: string; // ISO date string
|
lastPrayedFor?: string; // ISO date string
|
||||||
|
prayedDates?: string[]; // Array of dates when prayed for
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PrayerListProps {
|
interface PrayerListProps {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BellRing, X } from 'lucide-react';
|
import { BellRing, X } from 'lucide-react';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
import { db } from '@/lib/firebase';
|
||||||
|
import { doc, getDoc, setDoc } from 'firebase/firestore';
|
||||||
|
|
||||||
interface ReminderBannerProps {
|
interface ReminderBannerProps {
|
||||||
userEmail?: string;
|
userEmail?: string;
|
||||||
@@ -11,47 +14,70 @@ export default function ReminderBanner({ userEmail }: ReminderBannerProps) {
|
|||||||
const [showBanner, setShowBanner] = useState(false);
|
const [showBanner, setShowBanner] = useState(false);
|
||||||
const [reminderTime, setReminderTime] = useState('09:00');
|
const [reminderTime, setReminderTime] = useState('09:00');
|
||||||
const [isTime, setIsTime] = useState(false);
|
const [isTime, setIsTime] = useState(false);
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if reminders are enabled and if it's reminder time
|
if (!user) return;
|
||||||
const remindersEnabled = localStorage.getItem('prayerRemindersEnabled') === 'true';
|
|
||||||
const savedTime = localStorage.getItem('prayerReminderTime') || '09:00';
|
|
||||||
const lastReminderDate = localStorage.getItem('lastPrayerReminderDate');
|
|
||||||
|
|
||||||
setReminderTime(savedTime);
|
|
||||||
|
|
||||||
if (!remindersEnabled) return;
|
// Load settings from Firebase
|
||||||
|
const loadSettingsAndCheck = async () => {
|
||||||
|
try {
|
||||||
|
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
|
||||||
|
const settingsSnap = await getDoc(settingsRef);
|
||||||
|
|
||||||
|
if (!settingsSnap.exists()) return;
|
||||||
|
|
||||||
|
const settings = settingsSnap.data();
|
||||||
|
const remindersEnabled = settings.enabled || false;
|
||||||
|
const savedTime = settings.time || '09:00';
|
||||||
|
const lastReminderDate = settings.lastReminderDate;
|
||||||
|
|
||||||
|
setReminderTime(savedTime);
|
||||||
|
|
||||||
// Check if we should show the banner
|
if (!remindersEnabled) return;
|
||||||
const checkIfTimeToRemind = () => {
|
|
||||||
const now = new Date();
|
|
||||||
const [hours, minutes] = savedTime.split(':').map(Number);
|
|
||||||
const reminderTodayTime = new Date();
|
|
||||||
reminderTodayTime.setHours(hours, minutes, 0, 0);
|
|
||||||
|
|
||||||
// Check if current time is within 5 minutes of reminder time
|
// Check if we should show the banner
|
||||||
const timeDiff = Math.abs(now.getTime() - reminderTodayTime.getTime());
|
const checkIfTimeToRemind = () => {
|
||||||
const withinWindow = timeDiff < 5 * 60 * 1000; // 5 minute window
|
const now = new Date();
|
||||||
|
const [hours, minutes] = savedTime.split(':').map(Number);
|
||||||
|
const reminderTodayTime = new Date();
|
||||||
|
reminderTodayTime.setHours(hours, minutes, 0, 0);
|
||||||
|
|
||||||
if (withinWindow) {
|
// Check if current time is within 5 minutes of reminder time
|
||||||
// Check if we already showed reminder today
|
const timeDiff = Math.abs(now.getTime() - reminderTodayTime.getTime());
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const withinWindow = timeDiff < 5 * 60 * 1000; // 5 minute window
|
||||||
const lastDate = lastReminderDate?.split('T')[0];
|
|
||||||
|
|
||||||
if (lastDate !== today) {
|
if (withinWindow) {
|
||||||
setIsTime(true);
|
// Check if we already showed reminder today
|
||||||
setShowBanner(true);
|
const today = new Date().toISOString().split('T')[0];
|
||||||
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
|
const lastDate = lastReminderDate?.split('T')[0];
|
||||||
|
|
||||||
|
if (lastDate !== today) {
|
||||||
|
setIsTime(true);
|
||||||
|
setShowBanner(true);
|
||||||
|
|
||||||
|
// Update last reminder date in Firebase
|
||||||
|
setDoc(settingsRef, {
|
||||||
|
lastReminderDate: new Date().toISOString()
|
||||||
|
}, { merge: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkIfTimeToRemind();
|
||||||
|
|
||||||
|
// Check every minute
|
||||||
|
const interval = setInterval(checkIfTimeToRemind, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code !== 'unavailable') {
|
||||||
|
console.error('Error loading reminder settings:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkIfTimeToRemind();
|
loadSettingsAndCheck();
|
||||||
|
}, [user]);
|
||||||
// Check every minute
|
|
||||||
const interval = setInterval(checkIfTimeToRemind, 60000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!showBanner) return null;
|
if (!showBanner) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface Prayer {
|
|||||||
answerDetails?: string;
|
answerDetails?: string;
|
||||||
prayFrequency?: number; // 1-7 times per week
|
prayFrequency?: number; // 1-7 times per week
|
||||||
lastPrayedFor?: string; // ISO date string
|
lastPrayedFor?: string; // ISO date string
|
||||||
|
prayedDates?: string[]; // Array of dates when prayed for
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThingsToPrayForProps {
|
interface ThingsToPrayForProps {
|
||||||
@@ -85,7 +86,7 @@ export default function ThingsToPrayFor({ prayers, onEdit, onMarkPrayed }: Thing
|
|||||||
(new Date().getTime() - new Date(prayer.date).getTime()) / (1000 * 60 * 60 * 24)
|
(new Date().getTime() - new Date(prayer.date).getTime()) / (1000 * 60 * 60 * 24)
|
||||||
);
|
);
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const prayedToday = prayer.lastPrayedFor === today;
|
const prayedToday = prayer.prayedDates?.includes(today) || prayer.lastPrayedFor === today;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Simple auth state listener - no messaging complexity
|
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
let authResolved = false;
|
||||||
|
|
||||||
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -44,22 +44,36 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
signOut(auth);
|
signOut(auth);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
authResolved = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(user);
|
// If we get null on first call, wait a bit for session restoration
|
||||||
setLoading(false);
|
if (!user && !authResolved) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (mounted && !authResolved) {
|
||||||
|
setUser(null);
|
||||||
|
setLoading(false);
|
||||||
|
authResolved = true;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
setUser(user);
|
||||||
|
setLoading(false);
|
||||||
|
authResolved = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Timeout safety: force loading to false after 2 seconds
|
// Safety timeout: force loading to false after 4 seconds
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (mounted) {
|
if (mounted && !authResolved) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
authResolved = true;
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 4000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
|
|||||||
+14
-1
@@ -1,7 +1,7 @@
|
|||||||
// Import the functions you need from the SDKs you need
|
// Import the functions you need from the SDKs you need
|
||||||
import { initializeApp } from 'firebase/app';
|
import { initializeApp } from 'firebase/app';
|
||||||
import { getAuth } from 'firebase/auth';
|
import { getAuth } from 'firebase/auth';
|
||||||
import { getFirestore } from 'firebase/firestore';
|
import { getFirestore, enableIndexedDbPersistence, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
|
||||||
import { getStorage } from 'firebase/storage';
|
import { getStorage } from 'firebase/storage';
|
||||||
import { getAnalytics } from 'firebase/analytics';
|
import { getAnalytics } from 'firebase/analytics';
|
||||||
import { getMessaging, onMessage, getToken } from 'firebase/messaging';
|
import { getMessaging, onMessage, getToken } from 'firebase/messaging';
|
||||||
@@ -29,6 +29,19 @@ export const auth = getAuth(app);
|
|||||||
// Initialize Cloud Firestore and get a reference to the service
|
// Initialize Cloud Firestore and get a reference to the service
|
||||||
export const db = getFirestore(app);
|
export const db = getFirestore(app);
|
||||||
|
|
||||||
|
// Enable offline persistence
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
enableMultiTabIndexedDbPersistence(db).catch((err) => {
|
||||||
|
if (err.code === 'failed-precondition') {
|
||||||
|
// Multiple tabs open, persistence can only be enabled in one tab at a time
|
||||||
|
console.warn('Firestore: Multiple tabs open, persistence disabled');
|
||||||
|
} else if (err.code === 'unimplemented') {
|
||||||
|
// The current browser doesn't support persistence
|
||||||
|
console.warn('Firestore: Persistence not supported in this browser');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Firebase Storage and get a reference to the service
|
// Initialize Firebase Storage and get a reference to the service
|
||||||
export const storage = getStorage(app);
|
export const storage = getStorage(app);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user