Skip to content

Commit 2feee05

Browse files
authored
Merge pull request #187 from ekwe7/Email-push-notification-system-for-price-alerts
feat: implement price alerts and notification system
2 parents 6525efa + 28e915b commit 2feee05

8 files changed

Lines changed: 1091 additions & 7 deletions

File tree

src/app/alerts/page.tsx

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { useNotificationStore } from '@/store/notificationStore';
5+
import { Button } from '@/components/ui/button';
6+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
7+
import { Badge } from '@/components/ui/badge';
8+
import { Input } from '@/components/ui/input';
9+
import { Label } from '@/components/ui/label';
10+
import { Switch } from '@/components/ui/switch';
11+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
12+
import {
13+
Bell,
14+
BellRing,
15+
Trash2,
16+
TrendingUp,
17+
TrendingDown,
18+
AlertCircle,
19+
Mail,
20+
MailOff,
21+
Settings,
22+
X,
23+
ArrowLeft
24+
} from 'lucide-react';
25+
import { formatPrice } from '@/utils/searchUtils';
26+
import { PRICE_ALERT_TYPE_LABELS, type PriceAlertType } from '@/types/property';
27+
import Link from 'next/link';
28+
import { toast } from 'sonner';
29+
import { WalletConnector } from '@/components/WalletConnector';
30+
import { PriceAlertBell } from '@/components/PriceAlertBell';
31+
32+
export default function AlertsPage() {
33+
const {
34+
priceAlerts,
35+
priceAlertNotifications,
36+
removePriceAlert,
37+
togglePriceAlert,
38+
markPriceAlertNotificationAsRead,
39+
clearPriceAlertNotification,
40+
clearAllPriceAlertNotifications,
41+
settings,
42+
updateSettings,
43+
} = useNotificationStore();
44+
45+
const [activeTab, setActiveTab] = useState('alerts');
46+
47+
const unreadNotificationsCount = priceAlertNotifications.filter(n => !n.isRead).length;
48+
49+
const handleRemoveAlert = (alertId: string) => {
50+
removePriceAlert(alertId);
51+
toast.success('Alert removed successfully');
52+
};
53+
54+
const handleToggleAlert = (alertId: string) => {
55+
togglePriceAlert(alertId);
56+
};
57+
58+
const handleMarkAsRead = (notificationId: string) => {
59+
markPriceAlertNotificationAsRead(notificationId);
60+
};
61+
62+
const handleClearNotification = (notificationId: string) => {
63+
clearPriceAlertNotification(notificationId);
64+
};
65+
66+
const getAlertTypeIcon = (type: PriceAlertType) => {
67+
switch (type) {
68+
case 'above':
69+
return <TrendingUp className="w-5 h-5 text-green-600" />;
70+
case 'below':
71+
return <TrendingDown className="w-5 h-5 text-red-600" />;
72+
case 'change':
73+
return <AlertCircle className="w-5 h-5 text-orange-600" />;
74+
}
75+
};
76+
77+
return (
78+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
79+
{/* Header */}
80+
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-30">
81+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
82+
<div className="flex justify-between items-center h-16">
83+
<div className="flex items-center gap-4">
84+
<Link href="/properties">
85+
<Button variant="ghost" size="sm">
86+
<ArrowLeft className="w-4 h-4 mr-2" />
87+
Properties
88+
</Button>
89+
</Link>
90+
<div className="flex items-center gap-2">
91+
<div className="w-7 h-7 bg-blue-600 rounded-lg flex items-center justify-center">
92+
<span className="text-white font-bold text-xs">PC</span>
93+
</div>
94+
<span className="font-bold text-gray-900 dark:text-white">PropChain</span>
95+
</div>
96+
</div>
97+
<div className="flex items-center gap-2">
98+
<PriceAlertBell />
99+
<WalletConnector />
100+
</div>
101+
</div>
102+
</div>
103+
</header>
104+
105+
<div className="container mx-auto py-8 px-4">
106+
<div className="max-w-4xl mx-auto">
107+
{/* Page Header */}
108+
<div className="mb-8">
109+
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
110+
Price Alerts
111+
</h1>
112+
<p className="text-gray-600 dark:text-gray-400">
113+
Manage your property price alerts and notification preferences
114+
</p>
115+
</div>
116+
117+
<Tabs value={activeTab} onValueChange={setActiveTab}>
118+
<TabsList className="grid w-full grid-cols-3">
119+
<TabsTrigger value="alerts" className="flex items-center gap-2">
120+
<Bell className="w-4 h-4" />
121+
My Alerts
122+
{priceAlerts.length > 0 && (
123+
<Badge variant="secondary" className="ml-1">
124+
{priceAlerts.length}
125+
</Badge>
126+
)}
127+
</TabsTrigger>
128+
<TabsTrigger value="notifications" className="flex items-center gap-2">
129+
<BellRing className="w-4 h-4" />
130+
Notifications
131+
{unreadNotificationsCount > 0 && (
132+
<Badge variant="destructive" className="ml-1">
133+
{unreadNotificationsCount}
134+
</Badge>
135+
)}
136+
</TabsTrigger>
137+
<TabsTrigger value="settings" className="flex items-center gap-2">
138+
<Settings className="w-4 h-4" />
139+
Settings
140+
</TabsTrigger>
141+
</TabsList>
142+
143+
{/* Alerts Tab */}
144+
<TabsContent value="alerts" className="mt-6">
145+
{priceAlerts.length === 0 ? (
146+
<Card>
147+
<CardContent className="py-12 text-center">
148+
<Bell className="w-12 h-12 mx-auto mb-4 text-gray-400" />
149+
<h3 className="text-lg font-semibold mb-2">No alerts set</h3>
150+
<p className="text-gray-600 dark:text-gray-400 mb-4">
151+
Set up price alerts on property detail pages to get notified when prices change.
152+
</p>
153+
<Link href="/properties">
154+
<Button>Browse Properties</Button>
155+
</Link>
156+
</CardContent>
157+
</Card>
158+
) : (
159+
<div className="space-y-4">
160+
{priceAlerts.map((alert) => (
161+
<Card key={alert.id} className={!alert.isActive ? 'opacity-60' : ''}>
162+
<CardContent className="p-4">
163+
<div className="flex items-center justify-between">
164+
<div className="flex items-center gap-4">
165+
{getAlertTypeIcon(alert.alertType)}
166+
<div>
167+
<Link
168+
href={`/properties/${alert.propertyId}`}
169+
className="font-medium hover:underline"
170+
>
171+
{alert.propertyName}
172+
</Link>
173+
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
174+
<span>{PRICE_ALERT_TYPE_LABELS[alert.alertType]}</span>
175+
<span></span>
176+
<span>Target: {formatPrice(alert.targetPrice)}</span>
177+
<span></span>
178+
<span>Current: {formatPrice(alert.currentPrice)}</span>
179+
</div>
180+
</div>
181+
</div>
182+
<div className="flex items-center gap-3">
183+
{alert.isTriggered && (
184+
<Badge variant="default" className="bg-green-600">
185+
Triggered
186+
</Badge>
187+
)}
188+
<Switch
189+
checked={alert.isActive}
190+
onCheckedChange={() => handleToggleAlert(alert.id)}
191+
/>
192+
<Button
193+
variant="ghost"
194+
size="icon"
195+
onClick={() => handleRemoveAlert(alert.id)}
196+
>
197+
<Trash2 className="w-4 h-4 text-red-500" />
198+
</Button>
199+
</div>
200+
</div>
201+
</CardContent>
202+
</Card>
203+
))}
204+
</div>
205+
)}
206+
</TabsContent>
207+
208+
{/* Notifications Tab */}
209+
<TabsContent value="notifications" className="mt-6">
210+
{priceAlertNotifications.length === 0 ? (
211+
<Card>
212+
<CardContent className="py-12 text-center">
213+
<BellRing className="w-12 h-12 mx-auto mb-4 text-gray-400" />
214+
<h3 className="text-lg font-semibold mb-2">No notifications</h3>
215+
<p className="text-gray-600 dark:text-gray-400">
216+
You&apos;ll see notifications here when your price alerts are triggered.
217+
</p>
218+
</CardContent>
219+
</Card>
220+
) : (
221+
<>
222+
<div className="flex justify-end mb-4">
223+
<Button
224+
variant="outline"
225+
size="sm"
226+
onClick={() => {
227+
clearAllPriceAlertNotifications();
228+
toast.success('All notifications cleared');
229+
}}
230+
>
231+
Clear All
232+
</Button>
233+
</div>
234+
<div className="space-y-3">
235+
{priceAlertNotifications.map((notification) => (
236+
<Card
237+
key={notification.id}
238+
className={!notification.isRead ? 'border-l-4 border-l-blue-500' : ''}
239+
>
240+
<CardContent className="p-4">
241+
<div className="flex items-start justify-between">
242+
<div className="flex items-start gap-3">
243+
{getAlertTypeIcon(notification.alertType)}
244+
<div>
245+
<p className="font-medium">{notification.message}</p>
246+
<p className="text-sm text-gray-500 mt-1">
247+
{new Date(notification.createdAt).toLocaleString()}
248+
</p>
249+
</div>
250+
</div>
251+
<div className="flex items-center gap-2">
252+
{!notification.isRead && (
253+
<Button
254+
variant="ghost"
255+
size="sm"
256+
onClick={() => handleMarkAsRead(notification.id)}
257+
>
258+
Mark as read
259+
</Button>
260+
)}
261+
<Button
262+
variant="ghost"
263+
size="icon"
264+
onClick={() => handleClearNotification(notification.id)}
265+
>
266+
<X className="w-4 h-4" />
267+
</Button>
268+
</div>
269+
</div>
270+
</CardContent>
271+
</Card>
272+
))}
273+
</div>
274+
</>
275+
)}
276+
</TabsContent>
277+
278+
{/* Settings Tab */}
279+
<TabsContent value="settings" className="mt-6">
280+
<Card>
281+
<CardHeader>
282+
<CardTitle>Notification Settings</CardTitle>
283+
</CardHeader>
284+
<CardContent className="space-y-6">
285+
{/* Email Settings */}
286+
<div className="space-y-4">
287+
<div className="flex items-center justify-between">
288+
<div className="flex items-center gap-2">
289+
{settings.emailEnabled ? (
290+
<Mail className="w-5 h-5 text-green-600" />
291+
) : (
292+
<MailOff className="w-5 h-5 text-gray-400" />
293+
)}
294+
<div>
295+
<Label>Email Notifications</Label>
296+
<p className="text-sm text-gray-500">
297+
Receive alerts via email when triggered
298+
</p>
299+
</div>
300+
</div>
301+
<Switch
302+
checked={settings.emailEnabled}
303+
onCheckedChange={(checked) => updateSettings({ emailEnabled: checked })}
304+
/>
305+
</div>
306+
307+
{settings.emailEnabled && (
308+
<div className="ml-7">
309+
<Label htmlFor="email">Email Address</Label>
310+
<Input
311+
id="email"
312+
type="email"
313+
placeholder="your@email.com"
314+
value={settings.email}
315+
onChange={(e) => updateSettings({ email: e.target.value })}
316+
className="max-w-md"
317+
/>
318+
</div>
319+
)}
320+
</div>
321+
322+
<hr />
323+
324+
{/* In-App Settings */}
325+
<div className="flex items-center justify-between">
326+
<div className="flex items-center gap-2">
327+
<Bell className="w-5 h-5 text-blue-600" />
328+
<div>
329+
<Label>In-App Notifications</Label>
330+
<p className="text-sm text-gray-500">
331+
Show notifications in the app
332+
</p>
333+
</div>
334+
</div>
335+
<Switch
336+
checked={settings.inAppEnabled}
337+
onCheckedChange={(checked) => updateSettings({ inAppEnabled: checked })}
338+
/>
339+
</div>
340+
341+
<hr />
342+
343+
{/* Default Frequency */}
344+
<div className="space-y-2">
345+
<Label>Default Notification Frequency</Label>
346+
<div className="flex gap-2">
347+
{['instant', 'daily', 'weekly'].map((freq) => (
348+
<Button
349+
key={freq}
350+
variant={settings.defaultFrequency === freq ? 'default' : 'outline'}
351+
size="sm"
352+
onClick={() => updateSettings({
353+
defaultFrequency: freq as 'instant' | 'daily' | 'weekly'
354+
})}
355+
>
356+
{freq.charAt(0).toUpperCase() + freq.slice(1)}
357+
</Button>
358+
))}
359+
</div>
360+
</div>
361+
</CardContent>
362+
</Card>
363+
</TabsContent>
364+
</Tabs>
365+
</div>
366+
</div>
367+
</div>
368+
);
369+
}

0 commit comments

Comments
 (0)