Implement cross-device prayer synchronization
- Replace localStorage with Firestore for prayer persistence - Prayers now sync across all devices when logged in - Add userId to prayers for proper user association - Update archive/unarchive to work with Firestore - Update Firestore rules for user-specific prayer access - Remove localStorage dependencies
This commit is contained in:
1 parent
6b0713612d
commit
9f799397c8
3 files changed
+85
-32
No files matched your search
+5
-2
@@ -6,9 +6,12 @@ service cloud.firestore {
|
||||
allow read, write: if request.auth != null && request.auth.uid == userId;
|
||||
}
|
||||
|
||||
// Allow authenticated users to read/write prayers
|
||||
// Allow authenticated users to read/write their own prayers
|
||||
match /prayers/{prayerId} {
|
||||
allow read, write: if request.auth != null;
|
||||
allow read, write: if request.auth != null &&
|
||||
resource.data.userId == request.auth.uid;
|
||||
allow create: if request.auth != null &&
|
||||
request.resource.data.userId == request.auth.uid;
|
||||
}
|
||||
|
||||
// Allow server-side API access for scheduled emails (no auth required for GET)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
if(!self.define){let e,s={};const n=(n,a)=>(n=new URL(n+".js",a).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(a,t)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>n(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(a.map(e=>o[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/240-754372c10c8766c9.js",revision:"754372c10c8766c9"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/646.30443a55a43a3bfb.js",revision:"30443a55a43a3bfb"},{url:"/_next/static/chunks/7508b87c-1ab0c72b2a9e57da.js",revision:"1ab0c72b2a9e57da"},{url:"/_next/static/chunks/app/_global-error/page-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/_not-found/page-1123cfe416c748d0.js",revision:"1123cfe416c748d0"},{url:"/_next/static/chunks/app/api/send-email/route-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/api/send-scheduled-emails/route-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/layout-dc9d7c3da9cf59ef.js",revision:"dc9d7c3da9cf59ef"},{url:"/_next/static/chunks/app/page-99ceb58c98b8f8c5.js",revision:"99ceb58c98b8f8c5"},{url:"/_next/static/chunks/dd8162e8-b0853216adf21a86.js",revision:"b0853216adf21a86"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-7ddb690de3906442.js",revision:"7ddb690de3906442"},{url:"/_next/static/chunks/main-app-4f19d69ed4295336.js",revision:"4f19d69ed4295336"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-bcba0732671665d2.js",revision:"bcba0732671665d2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-167d4419e1b22999.js",revision:"167d4419e1b22999"},{url:"/_next/static/cnkXhILcfrDgcR4Vc6_X1/_buildManifest.js",revision:"a91cb247fd17096d2ba17603cfdf5fe7"},{url:"/_next/static/cnkXhILcfrDgcR4Vc6_X1/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/css/40e47386a48391ef.css",revision:"40e47386a48391ef"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/manifest.json",revision:"785fbc5941efd3caa27ade80c67bb5ef"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:a})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-aLine truncated
|
||||
if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(t,a)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>n(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(t.map(e=>o[e]||r(e))).then(e=>(a(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/UjtVxF0av0aMuBtu2Alok/_buildManifest.js",revision:"a91cb247fd17096d2ba17603cfdf5fe7"},{url:"/_next/static/UjtVxF0av0aMuBtu2Alok/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/240-6fd9f848c45d88ec.js",revision:"6fd9f848c45d88ec"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/646.30443a55a43a3bfb.js",revision:"30443a55a43a3bfb"},{url:"/_next/static/chunks/7508b87c-eed8443588913f8e.js",revision:"eed8443588913f8e"},{url:"/_next/static/chunks/app/_global-error/page-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/_not-found/page-1123cfe416c748d0.js",revision:"1123cfe416c748d0"},{url:"/_next/static/chunks/app/api/send-email/route-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/api/send-scheduled-emails/route-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/app/layout-dc9d7c3da9cf59ef.js",revision:"dc9d7c3da9cf59ef"},{url:"/_next/static/chunks/app/page-347b1a2f69277226.js",revision:"347b1a2f69277226"},{url:"/_next/static/chunks/dd8162e8-b0853216adf21a86.js",revision:"b0853216adf21a86"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-7ddb690de3906442.js",revision:"7ddb690de3906442"},{url:"/_next/static/chunks/main-app-4f19d69ed4295336.js",revision:"4f19d69ed4295336"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-bcba0732671665d2.js",revision:"bcba0732671665d2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7ff1880bba064291.js",revision:"7ff1880bba064291"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-167d4419e1b22999.js",revision:"167d4419e1b22999"},{url:"/_next/static/css/40e47386a48391ef.css",revision:"40e47386a48391ef"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/manifest.json",revision:"785fbc5941efd3caa27ade80c67bb5ef"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:t})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-aLine truncated
|
||||
+79
-29
@@ -5,7 +5,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||
import Login from '@/components/Login';
|
||||
import Admin from '@/components/Admin';
|
||||
import { db } from '@/lib/firebase';
|
||||
import { doc, setDoc } from 'firebase/firestore';
|
||||
import { doc, setDoc, collection, addDoc, getDocs, updateDoc, deleteDoc, query, where } from 'firebase/firestore';
|
||||
|
||||
interface Prayer {
|
||||
id: string;
|
||||
@@ -50,16 +50,34 @@ export default function Home() {
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('prayers');
|
||||
if (stored) {
|
||||
setPrayers(JSON.parse(stored));
|
||||
}
|
||||
}, []);
|
||||
// Load prayers from Firestore
|
||||
const loadPrayers = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const prayersRef = collection(db, 'prayers');
|
||||
const q = query(prayersRef, where('userId', '==', user.uid));
|
||||
const querySnapshot = await getDocs(q);
|
||||
|
||||
const loadedPrayers: Prayer[] = [];
|
||||
querySnapshot.forEach((doc) => {
|
||||
loadedPrayers.push({ id: doc.id, ...doc.data() } as Prayer);
|
||||
});
|
||||
|
||||
setPrayers(loadedPrayers);
|
||||
} catch (error) {
|
||||
console.error('Error loading prayers:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Load prayers when user changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('prayers', JSON.stringify(prayers));
|
||||
}, [prayers]);
|
||||
if (user) {
|
||||
loadPrayers();
|
||||
} else {
|
||||
setPrayers([]); // Clear prayers when user logs out
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// Auto-populate email field with user's email
|
||||
useEffect(() => {
|
||||
@@ -70,27 +88,46 @@ export default function Home() {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!user) {
|
||||
alert('Please log in to save prayers');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newPrayerData = {
|
||||
userId: user.uid,
|
||||
date: form.date,
|
||||
type: form.type,
|
||||
text: form.text,
|
||||
journal: form.journal,
|
||||
email: form.email,
|
||||
prayFor: form.prayFor,
|
||||
reminderFrequency: form.reminderFrequency,
|
||||
includeActiveSummary: form.includeActiveSummary,
|
||||
archived: false,
|
||||
};
|
||||
|
||||
// Save to Firestore
|
||||
const docRef = await addDoc(collection(db, 'prayers'), newPrayerData);
|
||||
|
||||
// Add to local state with Firestore ID
|
||||
const newPrayer: Prayer = {
|
||||
id: Date.now().toString(),
|
||||
userId: user?.uid || 'anonymous',
|
||||
...form,
|
||||
id: docRef.id,
|
||||
...newPrayerData,
|
||||
};
|
||||
setPrayers(prev => [...prev, newPrayer]);
|
||||
|
||||
// Store user reminder preferences in Firestore
|
||||
if (user && form.reminderFrequency !== 'never') {
|
||||
try {
|
||||
if (form.reminderFrequency !== 'never') {
|
||||
await setDoc(doc(db, 'users', user.uid), {
|
||||
email: form.email,
|
||||
reminderFrequency: form.reminderFrequency,
|
||||
includeActiveSummary: form.includeActiveSummary,
|
||||
lastEmailSent: null, // Will be set when first email is sent
|
||||
lastEmailSent: null,
|
||||
}, { merge: true });
|
||||
|
||||
console.log('✅ Reminder preferences saved');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to save reminder preferences:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset form
|
||||
@@ -99,11 +136,15 @@ export default function Home() {
|
||||
type: 'prayer',
|
||||
text: '',
|
||||
journal: '',
|
||||
email: user?.email || '',
|
||||
email: user.email || '',
|
||||
prayFor: '',
|
||||
reminderFrequency: 'never',
|
||||
includeActiveSummary: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving prayer:', error);
|
||||
alert('Failed to save prayer. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const activePrayers = prayers.filter(prayer => !prayer.archived);
|
||||
@@ -207,6 +248,23 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
// Archive/unarchive prayers in Firestore
|
||||
const toggleArchivePrayer = async (prayerId: string, archived: boolean) => {
|
||||
try {
|
||||
await updateDoc(doc(db, 'prayers', prayerId), {
|
||||
archived: archived
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setPrayers(prev => prev.map(p =>
|
||||
p.id === prayerId ? { ...p, archived: archived } : p
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('Error updating prayer archive status:', error);
|
||||
alert('Failed to update prayer. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || showLoadingScreen) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-800 flex items-center justify-center p-4">
|
||||
@@ -451,11 +509,7 @@ export default function Home() {
|
||||
<div className="flex justify-end mt-3 pt-2 border-t border-gray-600">
|
||||
{prayer.archived ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
setPrayers(prev => prev.map(p =>
|
||||
p.id === prayer.id ? { ...p, archived: false } : p
|
||||
));
|
||||
}}
|
||||
onClick={() => toggleArchivePrayer(prayer.id, false)}
|
||||
className="px-3 py-1 text-green-500 hover:text-green-400 text-sm font-medium hover:bg-gray-700 rounded transition-colors"
|
||||
title="Restore prayer"
|
||||
>
|
||||
@@ -463,11 +517,7 @@ export default function Home() {
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setPrayers(prev => prev.map(p =>
|
||||
p.id === prayer.id ? { ...p, archived: true } : p
|
||||
));
|
||||
}}
|
||||
onClick={() => toggleArchivePrayer(prayer.id, true)}
|
||||
className="px-3 py-1 text-red-500 hover:text-red-400 text-sm font-medium hover:bg-gray-700 rounded transition-colors"
|
||||
title="Archive prayer"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user