'use client' import { useState, useEffect, useMemo, useRef } from 'react' import { Bell, X, AlertTriangle, MapPin, Check } from 'lucide-react' import { findAffectedFarmers, generateNotificationMessage, type OutbreakLocation, type FarmerLocation, type Notification, } from '@/lib/notifications' import type { OutbreakReport } from '@/lib/outbreakReport' interface NotificationSystemProps { outbreaks: OutbreakReport[] currentFarmerLocation?: { lat: number; lng: number; crops: string[] } } const SEVERITY_SORT = { high: 0, medium: 1, low: 2 } as const export default function NotificationSystem({ outbreaks, currentFarmerLocation, }: NotificationSystemProps) { const [notifications, setNotifications] = useState([]) const [isOpen, setIsOpen] = useState(false) /** Outbreak IDs the user dismissed — otherwise the sync effect recreates them */ const [dismissedOutbreakIds, setDismissedOutbreakIds] = useState>(() => new Set()) const rootRef = useRef(null) useEffect(() => { if (!isOpen) return const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setIsOpen(false) } // Bubble-phase click avoids capture-phase pointer handlers stealing taps before button onClick runs. const onDocumentClick = (e: MouseEvent) => { const root = rootRef.current if (root && !root.contains(e.target as Node)) setIsOpen(false) } window.addEventListener('keydown', onKeyDown) document.addEventListener('click', onDocumentClick) return () => { window.removeEventListener('keydown', onKeyDown) document.removeEventListener('click', onDocumentClick) } }, [isOpen]) /** Demo persona when no farm is registered — must match `findAffectedFarmers` ids */ const DEMO_FARMER_ID = 'farmer-1' // Recompute when registration changes (useState initializer only runs once) const farmers = useMemo(() => [ // Arkansas area farmers { id: 'farmer-1', name: 'John Smith', email: 'john@example.com', lat: 35.5, // Near Russellville, AR (~20 miles) lng: -93.2, crops: ['corn', 'wheat', 'soybean'], radius: 250, }, { id: 'farmer-2', name: 'Sarah Johnson', email: 'sarah@example.com', lat: 35.1, // Within 250 miles of Russellville (~30 miles) lng: -92.8, crops: ['corn', 'rice'], radius: 250, }, { id: 'farmer-3', name: 'Mike Davis', email: 'mike@example.com', lat: 36.0, // Within 250 miles of Russellville (~50 miles) lng: -93.5, crops: ['corn', 'wheat', 'soybean'], radius: 250, }, { id: 'farmer-4', name: 'Arkansas Farm Co.', email: 'info@arkfarm.com', lat: 34.7, // Little Rock area - within 250 miles (~80 miles) lng: -92.3, crops: ['corn', 'soybean'], radius: 250, }, // California farmers { id: 'farmer-5', name: 'Central Valley Farms', email: 'contact@cvfarms.com', lat: 36.5, // Near Fresno, CA lng: -119.5, crops: ['wheat', 'corn'], radius: 250, }, { id: 'farmer-6', name: 'Golden State Agriculture', email: 'info@gsag.com', lat: 37.0, // Near Modesto, CA lng: -120.5, crops: ['wheat', 'corn', 'soybean'], radius: 250, }, // Texas farmers { id: 'farmer-7', name: 'Lone Star Crops', email: 'hello@lonestarcrops.com', lat: 32.0, // Near Abilene, TX lng: -99.5, crops: ['corn', 'wheat'], radius: 250, }, { id: 'farmer-8', name: 'Texas Grain Co.', email: 'info@texasgrain.com', lat: 31.5, // Near San Angelo, TX lng: -100.0, crops: ['corn', 'soybean'], radius: 250, }, // Iowa farmers { id: 'farmer-9', name: 'Iowa Corn Growers', email: 'contact@iowacorn.com', lat: 41.5, // Near Des Moines, IA lng: -93.0, crops: ['corn', 'soybean'], radius: 250, }, { id: 'farmer-10', name: 'Midwest Agriculture', email: 'info@midwestag.com', lat: 42.0, // Near Cedar Rapids, IA lng: -91.5, crops: ['corn', 'soybean', 'wheat'], radius: 250, }, // Illinois farmers { id: 'farmer-11', name: 'Prairie Farms', email: 'hello@prairiefarms.com', lat: 40.0, // Near Champaign, IL lng: -88.5, crops: ['corn', 'soybean'], radius: 250, }, // Kansas farmers { id: 'farmer-12', name: 'Kansas Wheat Growers', email: 'info@kswheat.com', lat: 38.5, // Near Wichita, KS lng: -98.0, crops: ['wheat', 'corn'], radius: 250, }, { id: 'farmer-13', name: 'Sunflower State Farms', email: 'contact@sunflowerfarms.com', lat: 39.0, // Near Topeka, KS lng: -95.5, crops: ['wheat', 'corn', 'soybean'], radius: 250, }, // Nebraska farmers { id: 'farmer-14', name: 'Cornhusker Agriculture', email: 'info@cornhuskerag.com', lat: 41.0, // Near Lincoln, NE lng: -96.5, crops: ['corn', 'soybean'], radius: 250, }, // North Carolina farmers { id: 'farmer-15', name: 'Carolina Crops', email: 'hello@carolinacrops.com', lat: 35.5, // Near Charlotte, NC lng: -80.5, crops: ['corn', 'soybean'], radius: 250, }, // Ohio farmers { id: 'farmer-16', name: 'Buckeye Farms', email: 'info@buckeyefarms.com', lat: 40.0, // Near Columbus, OH lng: -83.0, crops: ['corn', 'soybean', 'wheat'], radius: 250, }, // Add current user if location is available ...(currentFarmerLocation ? [ { id: 'current-user', name: 'You', lat: currentFarmerLocation.lat, lng: currentFarmerLocation.lng, crops: currentFarmerLocation.crops, radius: 250, } as FarmerLocation, ] : []), ], [currentFarmerLocation]) const toOutbreakLocation = (report: OutbreakReport): OutbreakLocation => ({ id: report.id, lat: report.lat, lng: report.lng, crop: report.crop, disease: report.disease, severity: report.severity, date: report.date, description: report.description, }) useEffect(() => { if (outbreaks.length === 0) { setNotifications([]) return } const targetFarmerId = currentFarmerLocation ? 'current-user' : DEMO_FARMER_ID setNotifications((prev) => { const readByOutbreak = new Map() const createdByOutbreak = new Map() for (const n of prev) { if (n.read) readByOutbreak.set(n.outbreakId, true) createdByOutbreak.set(n.outbreakId, n.createdAt) } const next: Notification[] = [] for (const report of outbreaks) { if (dismissedOutbreakIds.has(report.id)) continue const outbreakLocation = toOutbreakLocation(report) const affected = findAffectedFarmers(outbreakLocation, farmers) const match = affected.find((a) => a.farmer.id === targetFarmerId) if (!match) continue const verifiedTail = report.reporterVerified === true ? ' (verified farmer report)' : report.reporterVerified === false ? ' (unverified farmer report)' : ' (community report)' next.push({ id: `${report.id}-${targetFarmerId}`, farmerId: targetFarmerId, outbreakId: report.id, distance: match.distance, message: `${generateNotificationMessage(outbreakLocation, match.distance)}${verifiedTail}`, severity: outbreakLocation.severity, read: readByOutbreak.get(report.id) ?? false, createdAt: createdByOutbreak.get(report.id) ?? new Date().toISOString(), }) } next.sort((a, b) => { const s = SEVERITY_SORT[a.severity] - SEVERITY_SORT[b.severity] if (s !== 0) return s return a.distance - b.distance }) return next }) }, [outbreaks, farmers, currentFarmerLocation, dismissedOutbreakIds]) const markAsRead = (notificationId: string) => { setNotifications((prev) => prev.map((notif) => notif.id === notificationId ? { ...notif, read: true } : notif ) ) } const markAllAsRead = () => { setNotifications((prev) => prev.map((notif) => ({ ...notif, read: true }))) } const deleteNotification = (notificationId: string) => { setNotifications((prev) => { const n = prev.find((x) => x.id === notificationId) if (n) { setDismissedOutbreakIds((s) => new Set(s).add(n.outbreakId)) } return prev.filter((x) => x.id !== notificationId) }) } const unreadCount = notifications.filter((n) => !n.read).length return (
{isOpen && (

Crop alerts {unreadCount > 0 && ( {unreadCount} new )}

{unreadCount > 0 && ( )}
{notifications.length === 0 ? (

No alerts yet

You'll be notified when reported crop trouble is within 250 miles

) : (
{notifications.map((notification) => { const outbreak = outbreaks.find((o) => o.id === notification.outbreakId) as | OutbreakReport | undefined return (

{notification.message}

{outbreak && (
{outbreak.reporterVerified !== undefined && (

{outbreak.reporterVerified ? 'Verified farmer' : 'Unverified farmer'}

)}

{notification.distance.toFixed(1)} miles away

{new Date(notification.createdAt).toLocaleString()}

)}
{!notification.read && ( )}
) })}
)}
{notifications.length > 0 && (

Alerts within 250 miles of your farm (or demo location). Dismissed alerts stay hidden until refresh.

)}
)}
) }