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:
nmemmert committed 2026-01-19 10:11:57 -05:00
1 parent 2c29fc28d6
commit 67c5ff1b35
13 files changed
+419 -138

No files matched your search

+15
View File
@@ -4,6 +4,21 @@ service cloud.firestore {
// Allow authenticated users to read/write their own data
match /users/{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
+185 -40
View File
@@ -13,7 +13,7 @@ import AboutPage from '@/components/AboutPage';
import HomePage from '@/components/HomePage';
import AnsweredPrayers from '@/components/AnsweredPrayers';
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 toast from 'react-hot-toast';
@@ -26,13 +26,16 @@ interface Prayer {
answered?: boolean;
answerDetails?: string;
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() {
const [activeTab, setActiveTab] = useState<'home' | 'compose' | 'journal' | 'answered' | 'praying' | 'reflections' | 'about'>('home');
const [prayers, setPrayers] = useState<Prayer[]>([]);
const [editingPrayer, setEditingPrayer] = useState<Prayer | null>(null);
const [prayersLoading, setPrayersLoading] = useState(false);
const [mostPrayedFor, setMostPrayedFor] = useState<string>('');
// Form state
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
@@ -45,8 +48,13 @@ export default function Home() {
// Load prayers from Firestore
useEffect(() => {
if (!user) return;
if (!user) {
setPrayers([]);
setPrayersLoading(false);
return;
}
setPrayersLoading(true);
const loadPrayers = async () => {
try {
const prayersRef = collection(db, 'prayers');
@@ -62,31 +70,61 @@ export default function Home() {
} catch (error) {
console.error('Error loading prayers:', error);
toast.error('Failed to load prayers');
} finally {
setPrayersLoading(false);
}
};
loadPrayers();
fetchPrayerStatistics();
}, [user]);
// Load draft from localStorage
// Load draft from Firebase
useEffect(() => {
const draft = localStorage.getItem('prayerDraft');
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);
}
}
}, []);
if (!user) return;
// 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(() => {
localStorage.setItem('prayerDraft', JSON.stringify({ date, text, prayFor }));
}, [date, text, prayFor]);
if (!user) return;
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) => {
e.preventDefault();
@@ -137,7 +175,12 @@ export default function Home() {
setDate(new Date().toISOString().split('T')[0]);
setText('');
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) {
console.error('Error saving prayer:', error);
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) => {
if (!user) return;
try {
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), {
lastPrayedFor: today,
prayedDates: updatedDates,
});
// Update statistics if this prayer has a prayFor value
if (prayer.prayFor) {
await updatePrayerStatistics(prayer.prayFor);
}
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!');
} 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 (
<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="text-center">
<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">
{/* 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="w-24 h-24 mx-auto relative">
<div className="absolute inset-0 border-4 border-blue-500/30 rounded-full animate-spin"></div>
<div
className="absolute inset-2 border-4 border-transparent border-t-blue-400 rounded-full animate-spin"
style={{ animationDirection: 'reverse', animationDuration: '1.5s' }}
<div className="w-32 h-32 mx-auto relative">
{/* Outer rotating ring */}
<div className="absolute inset-0 border-4 border-blue-500/20 rounded-full"></div>
<div className="absolute inset-0 border-4 border-transparent border-t-blue-400 border-r-blue-400 rounded-full animate-spin"></div>
{/* 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>
{/* 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">
<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>
<img src="/icon-192x192.png" alt="Haver icon" className="w-12 h-12 mx-auto mb-3 rounded" />
<h1 className="text-2xl font-bold text-white mb-2 animate-pulse">Haver</h1>
<p className="text-gray-400 text-lg mb-4">Preparing your sacred space...</p>
{/* App Title */}
<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">
Haver
</h1>
{/* Loading Message */}
<p className="text-gray-300 text-lg mb-6 animate-pulse">
{loadingMessage}
</p>
<div className="max-w-xs mx-auto">
<blockquote className="text-gray-300 italic text-sm leading-relaxed">
{/* Inspirational Quote */}
<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."
</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 className="flex justify-center space-x-2 mt-8">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{ animationDelay: '0s' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
{/* Animated Dots */}
<div className="flex justify-center space-x-3">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></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>
@@ -237,6 +381,7 @@ export default function Home() {
onStartPraying={() => setActiveTab('compose')}
onSelectPrayer={handleSelectPrayer}
userName={user.displayName || user.email || undefined}
mostPrayedFor={mostPrayedFor}
/>
)}
+73 -22
View File
@@ -4,6 +4,8 @@ import React, { useState } from 'react';
import { useAuth } from '@/context/AuthContext';
import { Info, Smartphone, BellRing } from 'lucide-react';
import { APP_VERSION, WHATS_NEW_BRIEF } from '@/lib/version';
import { db } from '@/lib/firebase';
import { doc, getDoc, setDoc } from 'firebase/firestore';
interface AboutPageProps {
userEmail?: string;
@@ -55,49 +57,86 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
setNotificationPermission(Notification.permission);
}
// Load saved settings
const savedEnabled = localStorage.getItem('prayerRemindersEnabled') === 'true';
const savedTime = localStorage.getItem('prayerReminderTime') || '09:00';
setNotificationsEnabled(savedEnabled);
setReminderTime(savedTime);
// Schedule notification for supported platforms
if (savedEnabled && webPushSupport) {
scheduleNotification();
}
}, []);
// Load saved settings from Firebase
const loadSettings = async () => {
if (!user) return;
try {
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
const settingsSnap = await getDoc(settingsRef);
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 () => {
if (!supportsWebPush) return;
if (!supportsWebPush || !user) return;
if ('Notification' in window) {
const permission = await Notification.requestPermission();
setNotificationPermission(permission);
if (permission === 'granted') {
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();
}
}
};
const toggleNotifications = async () => {
if (!user) return;
if (supportsWebPush) {
if (!notificationsEnabled && notificationPermission !== 'granted') {
await requestNotificationPermission();
} else {
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) {
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
scheduleNotification();
}
}
} else {
// For iOS and unsupported platforms, just enable in-app reminders
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) => {
if (!user) return;
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) {
scheduleNotification();
}
};
const scheduleNotification = () => {
if (!supportsWebPush || !notificationsEnabled) return;
if (!supportsWebPush || !notificationsEnabled || !user) return;
// Calculate time until next reminder
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
if (timeUntilNotification > 0) {
setTimeout(() => {
if ('Notification' in window && Notification.permission === 'granted' && notificationsEnabled) {
if ('Notification' in window && Notification.permission === 'granted' && notificationsEnabled && user) {
new Notification('Time to Pray', {
body: 'Take a moment to connect with God and pray.',
icon: '/icon-192x192.png',
@@ -148,8 +196,11 @@ export default function AboutPage({ userEmail }: AboutPageProps) {
requireInteraction: false,
});
// Save reminder shown timestamp
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
// Save reminder shown timestamp to Firebase
const settingsRef = doc(db, 'users', user.uid, 'settings', 'notifications');
setDoc(settingsRef, {
lastReminderDate: new Date().toISOString()
}, { merge: true });
// Reschedule for next day
setTimeout(scheduleNotification, 24 * 60 * 60 * 1000);
+1
View File
@@ -10,6 +10,7 @@ interface Prayer {
answerDetails?: string;
prayFrequency?: number; // 1-7 times per week
lastPrayedFor?: string; // ISO date string
prayedDates?: string[]; // Array of dates when prayed for
}
interface AnsweredPrayersProps {
+12 -5
View File
@@ -9,6 +9,7 @@ interface Prayer {
text: string;
prayFor?: string;
lastPrayedFor?: string;
prayedDates?: string[];
}
interface CalendarViewProps {
@@ -24,13 +25,19 @@ export default function CalendarView({ prayers, onDayClick }: CalendarViewProps)
}, [prayers]);
const daysWithPrayedFor = useMemo(() => {
const dates = new Set<string>();
const dateCounts = new Map<string, number>();
prayers.forEach(p => {
if (p.lastPrayedFor) {
dates.add(p.lastPrayedFor);
// Check both old lastPrayedFor and new prayedDates array
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]);
const getDaysInMonth = (date: Date) => {
@@ -120,7 +127,7 @@ export default function CalendarView({ prayers, onDayClick }: CalendarViewProps)
? prayers.filter(p => p.date === dateString).length
: 0;
const prayedForCount = dateString
? prayers.filter(p => p.lastPrayedFor === dateString).length
? daysWithPrayedFor.get(dateString) || 0
: 0;
// Determine color based on activity type
+2 -2
View File
@@ -8,7 +8,7 @@ export default function Clock() {
useEffect(() => {
// Set initial time
setTime(new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
@@ -17,7 +17,7 @@ export default function Clock() {
// Update time every second
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
+14 -9
View File
@@ -2,7 +2,7 @@
import React, { useState } from 'react';
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 {
date: string;
@@ -85,14 +85,19 @@ export default function DailyEntry({
{/* Pray For */}
<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>
<input
type="text"
value={prayFor}
onChange={(e) => onPrayForChange(e.target.value)}
placeholder="Person, situation, or concern..."
list="prayfor-options"
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"
/>
<div className="relative">
<input
type="text"
value={prayFor}
onChange={(e) => onPrayForChange(e.target.value)}
placeholder="Person, situation, or concern..."
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 && (
<datalist id="prayfor-options">
{prayForOptions.map((opt) => (
+24 -22
View File
@@ -14,6 +14,7 @@ interface Prayer {
answerDetails?: string;
prayFrequency?: number; // 1-7 times per week
lastPrayedFor?: string; // ISO date string
prayedDates?: string[]; // Array of dates when prayed for
}
interface HomePageProps {
@@ -22,9 +23,10 @@ interface HomePageProps {
onStartPraying: () => void;
onSelectPrayer: (prayer: Prayer) => void;
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 todayPrayers = prayers.filter(p => p.date === today);
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;
// 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
const calculateStreak = () => {
@@ -65,8 +72,13 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
const activityDates = new Set<string>();
prayers.forEach(p => {
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)
: 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
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>
{/* Clock */}
<div className="max-w-md mx-auto mb-12">
<div className="text-center">
<Clock />
</div>
</div>
{/* 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" />
<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()}!
@@ -151,9 +147,15 @@ export default function HomePage({ prayers, todayCount, onStartPraying, onSelect
All caught up! Start a new prayer to keep your journey going.
</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' })}
</p>
{/* Clock - Below Date */}
<div className="mt-3 flex justify-center">
<div className="scale-75">
<Clock />
</div>
</div>
</div>
{/* Stats Grid */}
+1
View File
@@ -12,6 +12,7 @@ interface Prayer {
answered?: boolean;
prayFrequency?: number; // 1-7 times per week
lastPrayedFor?: string; // ISO date string
prayedDates?: string[]; // Array of dates when prayed for
}
interface PrayerListProps {
+56 -30
View File
@@ -2,6 +2,9 @@
import React, { useState, useEffect } from '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 {
userEmail?: string;
@@ -11,47 +14,70 @@ export default function ReminderBanner({ userEmail }: ReminderBannerProps) {
const [showBanner, setShowBanner] = useState(false);
const [reminderTime, setReminderTime] = useState('09:00');
const [isTime, setIsTime] = useState(false);
const { user } = useAuth();
useEffect(() => {
// Check if reminders are enabled and if it's reminder time
const remindersEnabled = localStorage.getItem('prayerRemindersEnabled') === 'true';
const savedTime = localStorage.getItem('prayerReminderTime') || '09:00';
const lastReminderDate = localStorage.getItem('lastPrayerReminderDate');
setReminderTime(savedTime);
if (!user) return;
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
const checkIfTimeToRemind = () => {
const now = new Date();
const [hours, minutes] = savedTime.split(':').map(Number);
const reminderTodayTime = new Date();
reminderTodayTime.setHours(hours, minutes, 0, 0);
if (!remindersEnabled) return;
// Check if current time is within 5 minutes of reminder time
const timeDiff = Math.abs(now.getTime() - reminderTodayTime.getTime());
const withinWindow = timeDiff < 5 * 60 * 1000; // 5 minute window
// Check if we should show the banner
const checkIfTimeToRemind = () => {
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 we already showed reminder today
const today = new Date().toISOString().split('T')[0];
const lastDate = lastReminderDate?.split('T')[0];
// Check if current time is within 5 minutes of reminder time
const timeDiff = Math.abs(now.getTime() - reminderTodayTime.getTime());
const withinWindow = timeDiff < 5 * 60 * 1000; // 5 minute window
if (lastDate !== today) {
setIsTime(true);
setShowBanner(true);
localStorage.setItem('lastPrayerReminderDate', new Date().toISOString());
if (withinWindow) {
// Check if we already showed reminder today
const today = new Date().toISOString().split('T')[0];
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();
// Check every minute
const interval = setInterval(checkIfTimeToRemind, 60000);
return () => clearInterval(interval);
}, []);
loadSettingsAndCheck();
}, [user]);
if (!showBanner) return null;
+2 -1
View File
@@ -13,6 +13,7 @@ interface Prayer {
answerDetails?: string;
prayFrequency?: number; // 1-7 times per week
lastPrayedFor?: string; // ISO date string
prayedDates?: string[]; // Array of dates when prayed for
}
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)
);
const today = new Date().toISOString().split('T')[0];
const prayedToday = prayer.lastPrayedFor === today;
const prayedToday = prayer.prayedDates?.includes(today) || prayer.lastPrayedFor === today;
return (
<div
+20 -6
View File
@@ -25,8 +25,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simple auth state listener - no messaging complexity
let mounted = true;
let authResolved = false;
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (mounted) {
@@ -44,22 +44,36 @@ export function AuthProvider({ children }: { children: ReactNode }) {
signOut(auth);
setUser(null);
setLoading(false);
authResolved = true;
return;
}
}
}
setUser(user);
setLoading(false);
// If we get null on first call, wait a bit for session restoration
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(() => {
if (mounted) {
if (mounted && !authResolved) {
setLoading(false);
authResolved = true;
}
}, 2000);
}, 4000);
return () => {
mounted = false;
+14 -1
View File
@@ -1,7 +1,7 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getFirestore, enableIndexedDbPersistence, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
import { getAnalytics } from 'firebase/analytics';
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
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
export const storage = getStorage(app);