Add enhanced search/filtering and push notifications for prayer reminders
This commit is contained in:
1 parent
aa3f42479f
commit
1a160135d7
5 files changed
+294
-38
No files matched your search
@@ -6,6 +6,8 @@
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"gcm_sender_id": "555011794707",
|
||||
"gcm_user_visible_only": true,
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192x192.png",
|
||||
|
||||
+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,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const r=e=>n(e,t),f={module:{uri:t},exports:c,require:r};s[t]=Promise.all(a.map(e=>f[e]||r(e))).then(e=>(i(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/AM_vEQgIshalV3dcCH-v2/_buildManifest.js",revision:"a91cb247fd17096d2ba17603cfdf5fe7"},{url:"/_next/static/AM_vEQgIshalV3dcCH-v2/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/104.b9111a893f0ad9d9.js",revision:"b9111a893f0ad9d9"},{url:"/_next/static/chunks/136-3bb757effc059d69.js",revision:"3bb757effc059d69"},{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/164f4fb6-a2df26e501616e62.js",revision:"a2df26e501616e62"},{url:"/_next/static/chunks/199.6d8b2513760bb8a6.js",revision:"6d8b2513760bb8a6"},{url:"/_next/static/chunks/2f0b94e8-4e6d0ae97e147ff2.js",revision:"4e6d0ae97e147ff2"},{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/745-f204172863dcffd8.js",revision:"f204172863dcffd8"},{url:"/_next/static/chunks/7508b87c-580313408902bce8.js",revision:"580313408902bce8"},{url:"/_next/static/chunks/804.7f3d4655e2446c89.js",revision:"7f3d4655e2446c89"},{url:"/_next/static/chunks/931.6ff3e03b1cc8cdf1.js",revision:"6ff3e03b1cc8cdf1"},{url:"/_next/static/chunks/995.a093d11da39e25d3.js",revision:"a093d11da39e25d3"},{url:"/_next/static/chunks/ad2866b8.f49c70da33da3b7b.js",revision:"f49c70da33da3b7b"},{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-c9bbec85d61ab45b.js",revision:"c9bbec85d61ab45b"},{url:"/_next/static/chunks/app/page-db2e38321401f568.js",revision:"db2e38321401f568"},{url:"/_next/static/chunks/bc98253f.ac7fe72f9e73e374.js",revision:"ac7fe72f9e73e374"},{url:"/_next/static/chunks/dd8162e8-bea2cd35813297c3.js",revision:"bea2cd35813297c3"},{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-8c242ea63ddea351.js",revision:"8c242ea63ddea351"},{url:"/_next/static/css/ad9b900c70cc4212.css",revision:"ad9b900c70cc4212"},{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:"/firebase-messaging-sw.js",revision:"04d66892653ef67e1151f1ec6c02420b"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192x192.png",revision:"bbbd7efc0e31088ae0a3aeede7608534"},{url:"/icon-512x512.png",revision:"400538d76556a3a8ee4fd3dfda7bc092"},{url:"/locales/en/commonLine truncated
|
||||
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,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const r=e=>n(e,t),o={module:{uri:t},exports:c,require:r};s[t]=Promise.all(a.map(e=>o[e]||r(e))).then(e=>(i(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/chunks/136-3bb757effc059d69.js",revision:"3bb757effc059d69"},{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/164f4fb6-a2df26e501616e62.js",revision:"a2df26e501616e62"},{url:"/_next/static/chunks/199.6d8b2513760bb8a6.js",revision:"6d8b2513760bb8a6"},{url:"/_next/static/chunks/2f0b94e8-4e6d0ae97e147ff2.js",revision:"4e6d0ae97e147ff2"},{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-580313408902bce8.js",revision:"580313408902bce8"},{url:"/_next/static/chunks/931.6ff3e03b1cc8cdf1.js",revision:"6ff3e03b1cc8cdf1"},{url:"/_next/static/chunks/944-92743b4c3cdd31b5.js",revision:"92743b4c3cdd31b5"},{url:"/_next/static/chunks/995.a093d11da39e25d3.js",revision:"a093d11da39e25d3"},{url:"/_next/static/chunks/ad2866b8.f49c70da33da3b7b.js",revision:"f49c70da33da3b7b"},{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-3a7f53b5adc6c74a.js",revision:"3a7f53b5adc6c74a"},{url:"/_next/static/chunks/app/page-cde09a561bebd963.js",revision:"cde09a561bebd963"},{url:"/_next/static/chunks/bc98253f.ac7fe72f9e73e374.js",revision:"ac7fe72f9e73e374"},{url:"/_next/static/chunks/dd8162e8-bea2cd35813297c3.js",revision:"bea2cd35813297c3"},{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-a837e9e033373f23.js",revision:"a837e9e033373f23"},{url:"/_next/static/css/2399cab8684792c9.css",revision:"2399cab8684792c9"},{url:"/_next/static/lz7o_GMdaM2vsk8rbh2E-/_buildManifest.js",revision:"a91cb247fd17096d2ba17603cfdf5fe7"},{url:"/_next/static/lz7o_GMdaM2vsk8rbh2E-/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{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:"/firebase-messaging-sw.js",revision:"04d66892653ef67e1151f1ec6c02420b"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192x192.png",revision:"bbbd7efc0e31088ae0a3aeede7608534"},{url:"/icon-512x512.png",revision:"400538d76556a3a8ee4fd3dfda7bc092"},{url:"/locales/en/common.json",revision:"54fe8b7b3b2f39e7604b236e7a480251"},{url:"/manifest.json",revision:"8f6840ae163d807596c5d5736a6300a3"},{url:"/next.svg",revision:"8e061864f388b47fLine truncated
|
||||
+169
-36
@@ -6,7 +6,7 @@ import Login from '@/components/Login';
|
||||
import Admin from '@/components/Admin';
|
||||
import Profile from '@/components/Profile';
|
||||
import { db } from '@/lib/firebase';
|
||||
import { doc, setDoc, collection, addDoc, getDocs, updateDoc, query, where } from 'firebase/firestore';
|
||||
import { doc, setDoc, collection, addDoc, getDocs, updateDoc, query, where, getDoc } from 'firebase/firestore';
|
||||
import emailjs from '@emailjs/browser';
|
||||
import jsPDF from 'jspdf';
|
||||
import { CSVLink } from 'react-csv';
|
||||
@@ -23,6 +23,8 @@ interface Prayer {
|
||||
archived?: boolean;
|
||||
reminderFrequency?: 'daily' | 'weekly' | 'monthly' | 'never';
|
||||
includeActiveSummary?: boolean;
|
||||
answered?: boolean;
|
||||
answeredDate?: string;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
@@ -47,6 +49,10 @@ export default function Home() {
|
||||
const { user, loading, logout, isAdmin } = useAuth();
|
||||
const [showLoadingScreen, setShowLoadingScreen] = useState(true);
|
||||
const [editingPrayer, setEditingPrayer] = useState<Prayer | null>(null);
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | 'prayer' | 'praise'>('all');
|
||||
const [userProfile, setUserProfile] = useState<any>({});
|
||||
|
||||
// Ensure loading screen shows for at least 5 seconds
|
||||
useEffect(() => {
|
||||
@@ -108,6 +114,23 @@ export default function Home() {
|
||||
loadSharedPrayers();
|
||||
}, []);
|
||||
|
||||
// Load user profile
|
||||
useEffect(() => {
|
||||
const loadUserProfile = async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
const docRef = doc(db, 'users', user.uid);
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
setUserProfile(docSnap.data());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading user profile:', error);
|
||||
}
|
||||
};
|
||||
loadUserProfile();
|
||||
}, [user]);
|
||||
|
||||
// Export functions
|
||||
const exportToPDF = (prayerList: Prayer[], title: string) => {
|
||||
const doc = new jsPDF();
|
||||
@@ -291,26 +314,37 @@ export default function Home() {
|
||||
const activePrayers = prayers.filter(prayer => !prayer.archived);
|
||||
const archivedPrayers = prayers.filter(prayer => prayer.archived);
|
||||
|
||||
// Filter prayers based on search term
|
||||
const filteredActivePrayers = activePrayers.filter(prayer =>
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const filteredArchivedPrayers = archivedPrayers.filter(prayer =>
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
// Enhanced filtering function
|
||||
const applyFilters = (prayerList: Prayer[]) => {
|
||||
return prayerList.filter(prayer => {
|
||||
// Keyword search
|
||||
const matchesSearch = !searchTerm ||
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.prayFor.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
// Date range filter
|
||||
const prayerDate = new Date(prayer.date);
|
||||
const fromDate = dateFrom ? new Date(dateFrom) : null;
|
||||
const toDateFilter = dateTo ? new Date(dateTo) : null;
|
||||
const matchesDateFrom = !fromDate || prayerDate >= fromDate;
|
||||
const matchesDateTo = !toDateFilter || prayerDate <= toDateFilter;
|
||||
|
||||
// Type filter
|
||||
const matchesType = typeFilter === 'all' || prayer.type === typeFilter;
|
||||
|
||||
return matchesSearch && matchesDateFrom && matchesDateTo && matchesType;
|
||||
});
|
||||
};
|
||||
|
||||
// Apply filters to each prayer list
|
||||
const filteredActivePrayers = applyFilters(activePrayers);
|
||||
const filteredArchivedPrayers = applyFilters(archivedPrayers);
|
||||
|
||||
// For search tab, combine all filtered prayers
|
||||
const allPrayers = [...activePrayers, ...archivedPrayers];
|
||||
const filteredSearchPrayers = allPrayers.filter(prayer =>
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
const filteredSearchPrayers = applyFilters(allPrayers);
|
||||
|
||||
// Pagination logic
|
||||
const totalActivePages = Math.ceil(filteredActivePrayers.length / itemsPerPage);
|
||||
@@ -422,6 +456,34 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
// Mark prayer as answered/unanswered
|
||||
const toggleAnsweredPrayer = async (prayerId: string, answered: boolean) => {
|
||||
try {
|
||||
const updateData: any = { answered };
|
||||
if (answered) {
|
||||
updateData.answeredDate = new Date().toISOString().split('T')[0];
|
||||
} else {
|
||||
updateData.answeredDate = null;
|
||||
}
|
||||
|
||||
await updateDoc(doc(db, 'prayers', prayerId), updateData);
|
||||
|
||||
// Update local state
|
||||
setPrayers(prev => prev.map(p =>
|
||||
p.id === prayerId ? { ...p, answered, answeredDate: answered ? new Date().toISOString().split('T')[0] : undefined } : p
|
||||
));
|
||||
|
||||
// Send notification if answered and notifications are enabled
|
||||
if (answered && userProfile.notificationsEnabled) {
|
||||
// This would trigger a server-side notification via Firebase Functions
|
||||
console.log('Prayer answered - notification would be sent');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating prayer answered status:', error);
|
||||
alert('Failed to update prayer. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// Start editing a prayer
|
||||
const startEditing = (prayer: Prayer) => {
|
||||
setEditingPrayer(prayer);
|
||||
@@ -686,23 +748,75 @@ export default function Home() {
|
||||
{activeTab !== 'add' && (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{activeTab === 'search' && (
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search all prayers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
}}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
autoFocus
|
||||
/>
|
||||
{searchTerm && (
|
||||
<p className="text-gray-400 text-sm mt-2">
|
||||
Found {filteredSearchPrayers.length} prayer{filteredSearchPrayers.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-6 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search keywords..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="p-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => {
|
||||
setTypeFilter(e.target.value as 'all' | 'prayer' | 'praise');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="p-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="prayer">Prayers Only</option>
|
||||
<option value="praise">Praises Only</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
placeholder="From date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => {
|
||||
setDateFrom(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="p-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
placeholder="To date"
|
||||
value={dateTo}
|
||||
onChange={(e) => {
|
||||
setDateTo(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="p-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('');
|
||||
setTypeFilter('all');
|
||||
setDateFrom('');
|
||||
setDateTo('');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded-lg"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
|
||||
{(searchTerm || typeFilter !== 'all' || dateFrom || dateTo) && (
|
||||
<p className="text-gray-400 text-sm">
|
||||
Found {filteredSearchPrayers.length} prayer{filteredSearchPrayers.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -757,7 +871,26 @@ export default function Home() {
|
||||
</div>
|
||||
)}
|
||||
{activeTab !== 'search' && (
|
||||
<div className="flex justify-end mt-3 pt-2 border-t border-gray-600">
|
||||
<div className="flex justify-between items-center mt-3 pt-2 border-t border-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
{prayer.answered && (
|
||||
<span className="text-green-400 text-sm font-medium flex items-center gap-1">
|
||||
✅ Answered {prayer.answeredDate && `on ${new Date(prayer.answeredDate).toLocaleDateString()}`}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => toggleAnsweredPrayer(prayer.id, !prayer.answered)}
|
||||
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
|
||||
prayer.answered
|
||||
? 'text-yellow-500 hover:text-yellow-400 hover:bg-gray-700'
|
||||
: 'text-green-500 hover:text-green-400 hover:bg-gray-700'
|
||||
}`}
|
||||
title={prayer.answered ? 'Mark as unanswered' : 'Mark as answered'}
|
||||
>
|
||||
{prayer.answered ? '↩️ Unmark' : '✅ Mark Answered'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{prayer.archived ? (
|
||||
<button
|
||||
onClick={() => toggleArchivePrayer(prayer.id, false)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { storage, db } from '@/lib/firebase';
|
||||
import { storage, db, requestNotificationPermission, onMessageListener } from '@/lib/firebase';
|
||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||
import { doc, getDoc, updateDoc } from 'firebase/firestore';
|
||||
import { useTheme } from 'next-themes';
|
||||
@@ -12,6 +12,8 @@ interface UserProfile {
|
||||
avatarUrl?: string;
|
||||
theme?: string;
|
||||
categories?: string[];
|
||||
notificationsEnabled?: boolean;
|
||||
fcmToken?: string;
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
@@ -20,6 +22,7 @@ export default function Profile() {
|
||||
const [profile, setProfile] = useState<UserProfile>({});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [newCategory, setNewCategory] = useState('');
|
||||
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
if (!user) return;
|
||||
@@ -28,6 +31,7 @@ export default function Profile() {
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
setProfile(docSnap.data() as UserProfile);
|
||||
setNotificationsEnabled(docSnap.data().notificationsEnabled);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error);
|
||||
@@ -40,6 +44,27 @@ export default function Profile() {
|
||||
}
|
||||
}, [user, loadProfile]);
|
||||
|
||||
// Load notification settings
|
||||
useEffect(() => {
|
||||
if (profile.notificationsEnabled !== undefined) {
|
||||
setNotificationsEnabled(profile.notificationsEnabled);
|
||||
}
|
||||
}, [profile.notificationsEnabled]);
|
||||
|
||||
// Listen for FCM messages
|
||||
useEffect(() => {
|
||||
if (notificationsEnabled) {
|
||||
onMessageListener().then((payload: any) => {
|
||||
console.log('Message received:', payload);
|
||||
// Handle incoming messages (e.g., show notification)
|
||||
new Notification(payload.notification.title, {
|
||||
body: payload.notification.body,
|
||||
icon: '/icon-192x192.png'
|
||||
});
|
||||
}).catch(err => console.log('Failed to listen for messages:', err));
|
||||
}
|
||||
}, [notificationsEnabled]);
|
||||
|
||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !user) return;
|
||||
@@ -66,6 +91,35 @@ export default function Profile() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationToggle = async (enabled: boolean) => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
const token = await requestNotificationPermission();
|
||||
if (token) {
|
||||
await updateDoc(doc(db, 'users', user.uid), {
|
||||
notificationsEnabled: true,
|
||||
fcmToken: token
|
||||
});
|
||||
setProfile(prev => ({ ...prev, notificationsEnabled: true, fcmToken: token }));
|
||||
setNotificationsEnabled(true);
|
||||
} else {
|
||||
alert('Notification permission denied. Please enable notifications in your browser settings.');
|
||||
}
|
||||
} else {
|
||||
await updateDoc(doc(db, 'users', user.uid), {
|
||||
notificationsEnabled: false,
|
||||
fcmToken: null
|
||||
});
|
||||
setProfile(prev => ({ ...prev, notificationsEnabled: false, fcmToken: undefined }));
|
||||
setNotificationsEnabled(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating notification settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addCategory = async () => {
|
||||
if (!newCategory.trim() || !user) return;
|
||||
const updatedCategories = [...(profile.categories || []), newCategory.trim()];
|
||||
@@ -81,6 +135,16 @@ export default function Profile() {
|
||||
setProfile(prev => ({ ...prev, categories: updatedCategories }));
|
||||
};
|
||||
|
||||
const toggleNotifications = async () => {
|
||||
if (!user) return;
|
||||
const newStatus = !notificationsEnabled;
|
||||
setNotificationsEnabled(newStatus);
|
||||
await updateDoc(doc(db, 'users', user.uid), { notificationsEnabled: newStatus });
|
||||
if (newStatus) {
|
||||
requestNotificationPermission();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto mb-8 p-6 bg-gray-800 rounded-lg">
|
||||
<h2 className="text-2xl font-bold mb-6">Profile</h2>
|
||||
@@ -115,6 +179,25 @@ export default function Profile() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2 text-lg">Notifications</label>
|
||||
<div className="flex items-center mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationsEnabled}
|
||||
onChange={(e) => handleNotificationToggle(e.target.checked)}
|
||||
className="w-6 h-6 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label className="ml-3 text-base font-medium">
|
||||
Enable push notifications for prayer reminders
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">
|
||||
Get notified about your prayer schedule and when prayers are answered.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2 text-lg">Prayer Categories</label>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getAuth } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
import { getStorage } from 'firebase/storage';
|
||||
import { getAnalytics } from 'firebase/analytics';
|
||||
import { getMessaging, onMessage, getToken } from 'firebase/messaging';
|
||||
|
||||
// TODO: Add SDKs for Firebase products that you want to use
|
||||
// https://firebase.google.com/docs/web/setup#available-libraries
|
||||
@@ -30,6 +31,16 @@ export const db = getFirestore(app);
|
||||
// Initialize Firebase Storage and get a reference to the service
|
||||
export const storage = getStorage(app);
|
||||
|
||||
// Initialize Firebase Cloud Messaging
|
||||
export let messaging: any = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
messaging = getMessaging(app);
|
||||
} catch (error) {
|
||||
console.warn('FCM not supported in this environment:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Firebase Analytics and get a reference to the service
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let analytics: any = null;
|
||||
@@ -45,4 +56,31 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
export { analytics };
|
||||
|
||||
// FCM utility functions
|
||||
export const requestNotificationPermission = async () => {
|
||||
if (!messaging) return null;
|
||||
|
||||
try {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission === 'granted') {
|
||||
const token = await getToken(messaging, {
|
||||
vapidKey: process.env.NEXT_PUBLIC_FCM_VAPID_KEY || 'your-vapid-key-here'
|
||||
});
|
||||
return token;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error requesting notification permission:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const onMessageListener = () =>
|
||||
new Promise((resolve) => {
|
||||
if (messaging) {
|
||||
onMessage(messaging, (payload) => {
|
||||
resolve(payload);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export { app };
|
||||
Reference in new issue
Block a user