feat: enhance search with fuzzy matching, word-based filtering, and answered/unanswered filters
This commit is contained in:
1 parent
bbc13237cc
commit
b013e972d2
1 file changed
+123
-42
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
|
||||||
interface Prayer {
|
interface Prayer {
|
||||||
id: string;
|
id: string;
|
||||||
date: string;
|
date: string;
|
||||||
text: string;
|
text: string;
|
||||||
prayFor?: string;
|
prayFor?: string;
|
||||||
|
answered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PrayerListProps {
|
interface PrayerListProps {
|
||||||
@@ -18,34 +19,60 @@ interface PrayerListProps {
|
|||||||
export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerListProps) {
|
export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerListProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortOrder, setSortOrder] = useState<'newest' | 'oldest'>('newest');
|
const [sortOrder, setSortOrder] = useState<'newest' | 'oldest'>('newest');
|
||||||
|
const [filterAnswered, setFilterAnswered] = useState<'all' | 'answered' | 'unanswered'>('all');
|
||||||
|
|
||||||
const filtered = prayers
|
// Fuzzy search helper - finds if search term matches any part of text
|
||||||
.filter(
|
const fuzzyMatch = (text: string, searchTerm: string): boolean => {
|
||||||
p =>
|
if (!searchTerm) return true;
|
||||||
p.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const searchLower = searchTerm.toLowerCase().trim();
|
||||||
p.prayFor?.toLowerCase().includes(searchTerm.toLowerCase())
|
const textLower = text.toLowerCase();
|
||||||
)
|
|
||||||
.sort((a, b) => {
|
// Direct substring match (most relevant)
|
||||||
const comparison = new Date(b.date).getTime() - new Date(a.date).getTime();
|
if (textLower.includes(searchLower)) return true;
|
||||||
return sortOrder === 'newest' ? comparison : -comparison;
|
|
||||||
});
|
// Split search into words and check if all words are found
|
||||||
|
const words = searchLower.split(/\s+/);
|
||||||
|
return words.every(word => textLower.includes(word));
|
||||||
|
};
|
||||||
|
|
||||||
const grouped = filtered.reduce((acc, prayer) => {
|
const filtered = useMemo(() => {
|
||||||
if (!acc[prayer.date]) acc[prayer.date] = [];
|
return prayers
|
||||||
acc[prayer.date].push(prayer);
|
.filter(p => {
|
||||||
return acc;
|
// Search filter
|
||||||
}, {} as Record<string, Prayer[]>);
|
const matchesSearch = fuzzyMatch(p.text, searchTerm) ||
|
||||||
|
fuzzyMatch(p.prayFor || '', searchTerm);
|
||||||
|
if (!matchesSearch) return false;
|
||||||
|
|
||||||
|
// Answered filter
|
||||||
|
if (filterAnswered === 'answered' && !p.answered) return false;
|
||||||
|
if (filterAnswered === 'unanswered' && p.answered) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const comparison = new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||||
|
return sortOrder === 'newest' ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
}, [prayers, searchTerm, sortOrder, filterAnswered]);
|
||||||
|
|
||||||
|
const grouped = useMemo(() => {
|
||||||
|
return filtered.reduce((acc, prayer) => {
|
||||||
|
if (!acc[prayer.date]) acc[prayer.date] = [];
|
||||||
|
acc[prayer.date].push(prayer);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, Prayer[]>);
|
||||||
|
}, [filtered]);
|
||||||
|
|
||||||
|
const resultCount = filtered.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 pt-20 px-4 pb-8">
|
<div className="min-h-screen bg-gradient-to-b from-slate-900 to-slate-800 pt-20 px-4 pb-8">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-slate-900 dark:text-white mb-2">
|
<h1 className="text-4xl font-bold text-white mb-2">Past Prayers</h1>
|
||||||
Past Prayers
|
<p className="text-slate-400">
|
||||||
</h1>
|
{resultCount} {resultCount === 1 ? 'prayer' : 'prayers'} found
|
||||||
<p className="text-slate-600 dark:text-slate-400">
|
|
||||||
Browse and reflect on your prayer history
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,31 +80,67 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
|
|||||||
<div className="space-y-4 mb-8">
|
<div className="space-y-4 mb-8">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search prayers..."
|
placeholder="Search by prayer text or person..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
className="w-full px-4 py-3 rounded-lg border border-slate-600 bg-slate-800 text-white placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Sort Options */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSortOrder('newest')}
|
onClick={() => setSortOrder('newest')}
|
||||||
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
|
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
sortOrder === 'newest'
|
sortOrder === 'newest'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
: 'bg-slate-200 dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-300 dark:hover:bg-slate-600'
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Newest First
|
📅 Newest
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSortOrder('oldest')}
|
onClick={() => setSortOrder('oldest')}
|
||||||
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
|
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
sortOrder === 'oldest'
|
sortOrder === 'oldest'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
: 'bg-slate-200 dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-300 dark:hover:bg-slate-600'
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Oldest First
|
📅 Oldest
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Answer Status Filter */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterAnswered('all')}
|
||||||
|
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors text-sm ${
|
||||||
|
filterAnswered === 'all'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Prayers
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterAnswered('answered')}
|
||||||
|
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors text-sm ${
|
||||||
|
filterAnswered === 'answered'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
✅ Answered
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterAnswered('unanswered')}
|
||||||
|
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors text-sm ${
|
||||||
|
filterAnswered === 'unanswered'
|
||||||
|
? 'bg-orange-600 text-white'
|
||||||
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
⏳ Waiting
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,9 +149,20 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-slate-600 dark:text-slate-400 text-lg">
|
<p className="text-slate-400 text-lg">
|
||||||
No prayers found. {searchTerm && 'Try adjusting your search.'}
|
{searchTerm || filterAnswered !== 'all' ? '❌ No prayers match your filters.' : '📝 No prayers yet. Start praying!'}
|
||||||
</p>
|
</p>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSearchTerm('');
|
||||||
|
setFilterAnswered('all');
|
||||||
|
}}
|
||||||
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
Object.keys(grouped)
|
Object.keys(grouped)
|
||||||
@@ -96,7 +170,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
|
|||||||
.reverse()
|
.reverse()
|
||||||
.map(date => (
|
.map(date => (
|
||||||
<div key={date}>
|
<div key={date}>
|
||||||
<h2 className="text-sm font-semibold text-slate-500 dark:text-slate-400 mb-3 uppercase tracking-wider">
|
<h2 className="text-sm font-semibold text-slate-400 mb-3 uppercase tracking-wider">
|
||||||
{new Date(date).toLocaleDateString('en-US', {
|
{new Date(date).toLocaleDateString('en-US', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -112,19 +186,26 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
|
|||||||
{grouped[date].map(prayer => (
|
{grouped[date].map(prayer => (
|
||||||
<div
|
<div
|
||||||
key={prayer.id}
|
key={prayer.id}
|
||||||
className="bg-white dark:bg-slate-800 rounded-lg p-6 shadow hover:shadow-lg transition-all cursor-pointer border border-slate-200 dark:border-slate-700"
|
className="bg-slate-800 rounded-lg p-6 shadow hover:shadow-lg transition-all cursor-pointer border border-slate-700 hover:border-blue-500"
|
||||||
onClick={() => onPrayerClick(prayer)}
|
onClick={() => onPrayerClick(prayer)}
|
||||||
>
|
>
|
||||||
{prayer.prayFor && (
|
<div className="flex items-start gap-3">
|
||||||
<p className="text-sm font-medium text-blue-600 dark:text-blue-400 mb-2">
|
{prayer.answered && (
|
||||||
Praying for: {prayer.prayFor}
|
<span className="text-2xl">✅</span>
|
||||||
</p>
|
)}
|
||||||
)}
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-slate-900 dark:text-white line-clamp-3">
|
{prayer.prayFor && (
|
||||||
{prayer.text}
|
<p className="text-sm font-medium text-blue-400 mb-2">
|
||||||
</p>
|
🙏 {prayer.prayFor}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-slate-200 line-clamp-3">
|
||||||
|
{prayer.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-4 flex justify-between items-center">
|
<div className="mt-4 flex justify-between items-center">
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
<span className="text-xs text-slate-500">
|
||||||
{new Date(date).toLocaleTimeString('en-US', {
|
{new Date(date).toLocaleTimeString('en-US', {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@@ -135,7 +216,7 @@ export default function PrayerList({ prayers, onPrayerClick, onEdit }: PrayerLis
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit(prayer);
|
onEdit(prayer);
|
||||||
}}
|
}}
|
||||||
className="text-xs px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 rounded hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
|
className="text-xs px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in new issue
Block a user