Skip to content

Commit d0c66b4

Browse files
authored
Merge pull request #177 from Ardecrownn/feature/property-alerts
Feature: Property alerts for new listings matching saved searches
2 parents 53c3c07 + ff56ddb commit d0c66b4

12 files changed

Lines changed: 1636 additions & 4 deletions

File tree

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
'use client';
2+
3+
import React, { useEffect, useState } from 'react';
4+
import Link from 'next/link';
5+
import { SavedSearchCard } from '@/components/SavedSearchCard';
6+
import { WalletConnector } from '@/components/WalletConnector';
7+
import { useSavedSearchStore } from '@/store/savedSearchStore';
8+
import { useWalletStore } from '@/store/walletStore';
9+
import { SavedSearch, NotificationFrequency } from '@/types/property';
10+
import { toast } from 'sonner';
11+
import { Button } from '@/components/ui/button';
12+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
13+
import { Badge } from '@/components/ui/badge';
14+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
15+
import { Input } from '@/components/ui/input';
16+
import { Search, Filter, Settings, Bell, Mail, Bookmark } from 'lucide-react';
17+
18+
function SavedSearchesContent() {
19+
const { address } = useWalletStore();
20+
const {
21+
searches,
22+
isLoading,
23+
error,
24+
loadSearches,
25+
removeSearch
26+
} = useSavedSearchStore();
27+
28+
const [searchTerm, setSearchTerm] = useState('');
29+
const [frequencyFilter, setFrequencyFilter] = useState<NotificationFrequency | 'all'>('all');
30+
const [sortBy, setSortBy] = useState<'name' | 'created' | 'frequency'>('created');
31+
32+
useEffect(() => {
33+
if (address) {
34+
loadSearches(address);
35+
}
36+
}, [address, loadSearches]);
37+
38+
const filteredAndSortedSearches = searches
39+
.filter(search => {
40+
const matchesSearch = search.name.toLowerCase().includes(searchTerm.toLowerCase());
41+
const matchesFrequency = frequencyFilter === 'all' || search.notificationFrequency === frequencyFilter;
42+
return matchesSearch && matchesFrequency;
43+
})
44+
.sort((a, b) => {
45+
switch (sortBy) {
46+
case 'name':
47+
return a.name.localeCompare(b.name);
48+
case 'created':
49+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
50+
case 'frequency':
51+
return a.notificationFrequency.localeCompare(b.notificationFrequency);
52+
default:
53+
return 0;
54+
}
55+
});
56+
57+
const handleDeleteSearch = async (searchId: string) => {
58+
if (!address) return;
59+
60+
try {
61+
await removeSearch(searchId, address);
62+
toast.success('Search deleted successfully');
63+
} catch (error) {
64+
toast.error('Failed to delete search');
65+
console.error('Delete search error:', error);
66+
}
67+
};
68+
69+
if (!address) {
70+
return (
71+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
72+
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
73+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
74+
<div className="flex justify-between items-center h-16">
75+
<Link href="/" className="flex items-center gap-3">
76+
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
77+
<span className="text-white font-bold text-sm">PC</span>
78+
</div>
79+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
80+
PropChain
81+
</h1>
82+
</Link>
83+
<WalletConnector />
84+
</div>
85+
</div>
86+
</header>
87+
88+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
89+
<div className="text-center py-20">
90+
<Bookmark className="w-16 h-16 text-gray-400 mx-auto mb-4" />
91+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
92+
Connect Your Wallet
93+
</h2>
94+
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
95+
Please connect your wallet to manage your saved property searches.
96+
</p>
97+
</div>
98+
</div>
99+
</div>
100+
);
101+
}
102+
103+
return (
104+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
105+
{/* Header */}
106+
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
107+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
108+
<div className="flex justify-between items-center h-16">
109+
<Link href="/" className="flex items-center gap-3">
110+
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
111+
<span className="text-white font-bold text-sm">PC</span>
112+
</div>
113+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
114+
PropChain
115+
</h1>
116+
</Link>
117+
<WalletConnector />
118+
</div>
119+
</div>
120+
</header>
121+
122+
{/* Main Content */}
123+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
124+
{/* Page Header */}
125+
<div className="mb-8">
126+
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
127+
Saved Searches
128+
</h1>
129+
<p className="text-gray-600 dark:text-gray-400">
130+
Manage your saved property searches and notification preferences.
131+
</p>
132+
</div>
133+
134+
{/* Filters and Controls */}
135+
<Card className="mb-6">
136+
<CardHeader>
137+
<CardTitle className="flex items-center gap-2">
138+
<Filter className="w-5 h-5" />
139+
Filters & Search
140+
</CardTitle>
141+
</CardHeader>
142+
<CardContent>
143+
<div className="flex flex-col sm:flex-row gap-4">
144+
{/* Search */}
145+
<div className="flex-1">
146+
<div className="relative">
147+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
148+
<Input
149+
placeholder="Search saved searches..."
150+
value={searchTerm}
151+
onChange={(e) => setSearchTerm(e.target.value)}
152+
className="pl-10"
153+
/>
154+
</div>
155+
</div>
156+
157+
{/* Frequency Filter */}
158+
<Select
159+
value={frequencyFilter}
160+
onValueChange={(value: NotificationFrequency | 'all') => setFrequencyFilter(value)}
161+
>
162+
<SelectTrigger className="w-full sm:w-48">
163+
<SelectValue placeholder="Filter by frequency" />
164+
</SelectTrigger>
165+
<SelectContent>
166+
<SelectItem value="all">All Frequencies</SelectItem>
167+
<SelectItem value="instant">Instant</SelectItem>
168+
<SelectItem value="daily">Daily</SelectItem>
169+
<SelectItem value="weekly">Weekly</SelectItem>
170+
</SelectContent>
171+
</Select>
172+
173+
{/* Sort */}
174+
<Select
175+
value={sortBy}
176+
onValueChange={(value: 'name' | 'created' | 'frequency') => setSortBy(value)}
177+
>
178+
<SelectTrigger className="w-full sm:w-48">
179+
<SelectValue placeholder="Sort by" />
180+
</SelectTrigger>
181+
<SelectContent>
182+
<SelectItem value="created">Date Created</SelectItem>
183+
<SelectItem value="name">Name</SelectItem>
184+
<SelectItem value="frequency">Frequency</SelectItem>
185+
</SelectContent>
186+
</Select>
187+
</div>
188+
</CardContent>
189+
</Card>
190+
191+
{/* Stats */}
192+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
193+
<Card>
194+
<CardContent className="p-4">
195+
<div className="flex items-center gap-2">
196+
<Bookmark className="w-5 h-5 text-blue-600" />
197+
<div>
198+
<p className="text-sm text-gray-600 dark:text-gray-400">Total Searches</p>
199+
<p className="text-2xl font-bold text-gray-900 dark:text-white">
200+
{searches.length}
201+
</p>
202+
</div>
203+
</div>
204+
</CardContent>
205+
</Card>
206+
207+
<Card>
208+
<CardContent className="p-4">
209+
<div className="flex items-center gap-2">
210+
<Bell className="w-5 h-5 text-green-600" />
211+
<div>
212+
<p className="text-sm text-gray-600 dark:text-gray-400">Active Alerts</p>
213+
<p className="text-2xl font-bold text-gray-900 dark:text-white">
214+
{searches.filter(s => s.isActive).length}
215+
</p>
216+
</div>
217+
</div>
218+
</CardContent>
219+
</Card>
220+
221+
<Card>
222+
<CardContent className="p-4">
223+
<div className="flex items-center gap-2">
224+
<Mail className="w-5 h-5 text-purple-600" />
225+
<div>
226+
<p className="text-sm text-gray-600 dark:text-gray-400">Email Enabled</p>
227+
<p className="text-2xl font-bold text-gray-900 dark:text-white">
228+
{searches.filter(s => s.emailNotifications).length}
229+
</p>
230+
</div>
231+
</div>
232+
</CardContent>
233+
</Card>
234+
</div>
235+
236+
{/* Error State */}
237+
{error && (
238+
<Card className="mb-6 border-red-200 dark:border-red-800">
239+
<CardContent className="p-4">
240+
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
241+
<Settings className="w-5 h-5" />
242+
<p>{error}</p>
243+
</div>
244+
</CardContent>
245+
</Card>
246+
)}
247+
248+
{/* Loading State */}
249+
{isLoading && (
250+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
251+
{[...Array(6)].map((_, i) => (
252+
<Card key={i} className="animate-pulse">
253+
<CardHeader>
254+
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4" />
255+
<div className="h-3 bg-gray-300 dark:bg-gray-700 rounded w-1/2" />
256+
</CardHeader>
257+
<CardContent>
258+
<div className="space-y-2">
259+
<div className="h-3 bg-gray-300 dark:bg-gray-700 rounded" />
260+
<div className="h-3 bg-gray-300 dark:bg-gray-700 rounded w-5/6" />
261+
<div className="h-3 bg-gray-300 dark:bg-gray-700 rounded w-4/6" />
262+
</div>
263+
</CardContent>
264+
</Card>
265+
))}
266+
</div>
267+
)}
268+
269+
{/* Empty State */}
270+
{!isLoading && filteredAndSortedSearches.length === 0 && (
271+
<Card>
272+
<CardContent className="text-center py-20">
273+
<Bookmark className="w-16 h-16 text-gray-400 mx-auto mb-4" />
274+
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
275+
{searches.length === 0 ? 'No Saved Searches Yet' : 'No Matching Searches'}
276+
</h3>
277+
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto mb-6">
278+
{searches.length === 0
279+
? 'Start searching for properties and save your searches to get notified about new listings.'
280+
: 'Try adjusting your filters to find your saved searches.'
281+
}
282+
</p>
283+
{searches.length === 0 && (
284+
<Link href="/properties">
285+
<Button>Browse Properties</Button>
286+
</Link>
287+
)}
288+
</CardContent>
289+
</Card>
290+
)}
291+
292+
{/* Saved Searches Grid */}
293+
{!isLoading && filteredAndSortedSearches.length > 0 && (
294+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
295+
{filteredAndSortedSearches.map((search) => (
296+
<SavedSearchCard
297+
key={search.id}
298+
search={search}
299+
onDelete={() => handleDeleteSearch(search.id)}
300+
/>
301+
))}
302+
</div>
303+
)}
304+
</div>
305+
</div>
306+
);
307+
}
308+
309+
export default function SavedSearchesPage() {
310+
return (
311+
<SavedSearchesContent />
312+
);
313+
}

src/app/properties/page.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,29 @@ import { PropertySearch } from '@/components/PropertySearch';
55
import { FilterSidebar } from '@/components/FilterSidebar';
66
import { SearchResults } from '@/components/SearchResults';
77
import { WalletConnector } from '@/components/WalletConnector';
8+
import { NotificationCenter } from '@/components/NotificationCenter';
9+
import { Button } from '@/components/ui/button';
810
import { usePropertySearch } from '@/hooks/usePropertySearch';
911
import { useSearchStore } from '@/store/searchStore';
12+
import { useNotificationStore } from '@/store/notificationStore';
13+
import { useWalletStore } from '@/store/walletStore';
14+
import { useNotificationChecker } from '@/hooks/useNotificationChecker';
15+
import { notificationService } from '@/lib/notificationService';
1016
import Link from 'next/link';
1117

1218
function PropertiesContent() {
1319
const { viewMode: storeViewMode, setViewMode: setStoreViewMode } = useSearchStore();
20+
const { address } = useWalletStore();
21+
const {
22+
alerts,
23+
addAlert,
24+
markAsRead,
25+
markAllAsRead,
26+
clearAlert
27+
} = useNotificationStore();
28+
29+
// Set up notification checker
30+
useNotificationChecker();
1431

1532
// Ensure viewMode is only 'grid' or 'list' for now (map view not implemented yet)
1633
const viewMode: 'grid' | 'list' = storeViewMode === 'map' ? 'grid' : storeViewMode;
@@ -45,7 +62,20 @@ function PropertiesContent() {
4562
PropChain
4663
</h1>
4764
</Link>
48-
<WalletConnector />
65+
<div className="flex items-center gap-3">
66+
<Link href="/dashboard/saved-searches">
67+
<Button variant="ghost" size="sm">
68+
Saved Searches
69+
</Button>
70+
</Link>
71+
<NotificationCenter
72+
alerts={alerts}
73+
onMarkAsRead={markAsRead}
74+
onMarkAllAsRead={markAllAsRead}
75+
onClearAlert={clearAlert}
76+
/>
77+
<WalletConnector />
78+
</div>
4979
</div>
5080
</div>
5181
</header>
@@ -82,6 +112,7 @@ function PropertiesContent() {
82112
sortBy={sortBy}
83113
page={page}
84114
totalPages={totalPages}
115+
filters={filters}
85116
onViewModeChange={setViewMode}
86117
onSortChange={setSortBy}
87118
onPageChange={setPage}

0 commit comments

Comments
 (0)