File size: 17,948 Bytes
889dd1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965ef8f
 
 
 
 
 
889dd1b
 
 
965ef8f
889dd1b
965ef8f
889dd1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965ef8f
889dd1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965ef8f
889dd1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965ef8f
889dd1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965ef8f
889dd1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
'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<Notification[]>([])
  const [isOpen, setIsOpen] = useState(false)
  /** Outbreak IDs the user dismissed — otherwise the sync effect recreates them */
  const [dismissedOutbreakIds, setDismissedOutbreakIds] = useState<Set<string>>(() => new Set())
  const rootRef = useRef<HTMLDivElement>(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<FarmerLocation[]>(() => [
    // 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<string, boolean>()
      const createdByOutbreak = new Map<string, string>()
      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 (
    <div
      ref={rootRef}
      className="relative z-[200] inline-flex shrink-0 items-center justify-center self-center"
    >
      <button
        type="button"
        onClick={() => setIsOpen(!isOpen)}
        className={`touch-manipulation relative flex h-9 w-9 items-center justify-center rounded-full border bg-white/70 shadow-sm backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md ${
          isOpen
            ? 'border-primary-300 text-primary-700'
            : 'border-field-soil/15 text-primary-900 hover:border-primary-300 hover:text-primary-700'
        }`}
        aria-label="Crop alerts"
        aria-expanded={isOpen}
        aria-haspopup="dialog"
      >
        <Bell className="h-5 w-5" />
        {unreadCount > 0 && (
          <span className="absolute -right-0.5 -top-0.5 flex h-5 min-w-[1.25rem] items-center justify-center rounded-full border-2 border-white bg-red-600 px-1 text-[10px] font-bold leading-none text-white shadow-sm">
            {unreadCount > 9 ? '9+' : unreadCount}
          </span>
        )}
      </button>
      {isOpen && (
        <div
          role="dialog"
          aria-modal="true"
          aria-labelledby="disease-alerts-title"
          className="absolute right-0 top-[calc(100%+10px)] flex max-h-[min(420px,70dvh)] w-[min(22rem,calc(100vw-1.5rem))] flex-col overflow-hidden rounded-2xl border border-slate-300 bg-white shadow-xl ring-1 ring-slate-900/10"
        >
              <div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 bg-white p-4">
                <div className="min-w-0 flex-1">
                  <h3
                    id="disease-alerts-title"
                    className="flex flex-wrap items-center gap-x-2 gap-y-1.5 text-lg font-bold leading-tight text-slate-900 sm:text-xl"
                  >
                    <span className="inline-flex shrink-0 items-center gap-2">
                      <Bell className="h-5 w-5 shrink-0 text-primary-700" />
                      <span>Crop alerts</span>
                    </span>
                    {unreadCount > 0 && (
                      <span className="inline-flex shrink-0 items-center rounded-full bg-red-600 px-2 py-0.5 text-[11px] font-bold uppercase tracking-wide text-white shadow-sm">
                        {unreadCount} new
                      </span>
                    )}
                  </h3>
                </div>
                <div className="flex shrink-0 items-center gap-1.5 sm:gap-2">
                  {unreadCount > 0 && (
                    <button
                      type="button"
                      onClick={markAllAsRead}
                      className="whitespace-nowrap rounded-lg px-2 py-1.5 text-xs font-semibold text-primary-700 transition-colors hover:bg-primary-50"
                    >
                      Mark all read
                    </button>
                  )}
                  <button
                    type="button"
                    onClick={() => setIsOpen(false)}
                    className="rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-800"
                    aria-label="Close notifications"
                  >
                    <X className="h-5 w-5" />
                  </button>
                </div>
              </div>

              <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-white p-2">
                {notifications.length === 0 ? (
                  <div className="py-10 text-center text-slate-600">
                    <Bell className="mx-auto mb-3 h-12 w-12 text-slate-400" />
                    <p className="font-semibold text-slate-800">No alerts yet</p>
                    <p className="mt-1 text-sm text-slate-500">
                      You&apos;ll be notified when reported crop trouble is within 250 miles
                    </p>
                  </div>
                ) : (
                  <div className="space-y-2">
                    {notifications.map((notification) => {
                      const outbreak = outbreaks.find((o) => o.id === notification.outbreakId) as
                        | OutbreakReport
                        | undefined
                      return (
                        <div
                          key={notification.id}
                          className={`rounded-xl border p-4 transition-all ${
                            notification.read
                              ? 'border-slate-200 bg-slate-50'
                              : 'border-red-200 bg-red-50/95 shadow-sm'
                          }`}
                        >
                          <div className="flex items-start justify-between gap-3">
                            <div className="min-w-0 flex-1">
                              <div className="mb-2 flex gap-2">
                                <AlertTriangle
                                  className={`mt-0.5 h-5 w-5 shrink-0 ${
                                    notification.severity === 'high'
                                      ? 'text-red-600'
                                      : notification.severity === 'medium'
                                        ? 'text-orange-600'
                                        : 'text-yellow-600'
                                  }`}
                                />
                                <p className="text-sm font-bold leading-snug text-slate-900">
                                  {notification.message}
                                </p>
                              </div>
                              {outbreak && (
                                <div className="mt-2 space-y-1 pl-0 sm:pl-7">
                                  {outbreak.reporterVerified !== undefined && (
                                    <p className="text-xs">
                                      <span
                                        className={`inline-block rounded-md border px-2 py-0.5 font-bold ${
                                          outbreak.reporterVerified
                                            ? 'border-primary-200 bg-primary-100 text-primary-900'
                                            : 'border-slate-200 bg-slate-100 text-slate-700'
                                        }`}
                                      >
                                        {outbreak.reporterVerified ? 'Verified farmer' : 'Unverified farmer'}
                                      </span>
                                    </p>
                                  )}
                                  <p className="flex items-center gap-1 text-xs text-slate-600">
                                    <MapPin className="h-3 w-3 shrink-0" />
                                    {notification.distance.toFixed(1)} miles away
                                  </p>
                                  <p className="text-xs text-slate-500">
                                    {new Date(notification.createdAt).toLocaleString()}
                                  </p>
                                </div>
                              )}
                            </div>
                            <div className="flex shrink-0 items-start gap-1">
                              {!notification.read && (
                                <button
                                  type="button"
                                  onClick={() => markAsRead(notification.id)}
                                  className="rounded-lg p-1.5 text-primary-700 transition-colors hover:bg-white"
                                  title="Mark as read"
                                >
                                  <Check className="h-4 w-4" />
                                </button>
                              )}
                              <button
                                type="button"
                                onClick={() => deleteNotification(notification.id)}
                                className="rounded-lg p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-slate-800"
                                title="Delete"
                              >
                                <X className="h-4 w-4" />
                              </button>
                            </div>
                          </div>
                        </div>
                      )
                    })}
                  </div>
                )}
              </div>

              {notifications.length > 0 && (
                <div className="shrink-0 border-t border-slate-200 bg-slate-100/90 p-3">
                  <p className="text-center text-xs text-slate-600">
                    Alerts within 250 miles of your farm (or demo location). Dismissed alerts stay hidden until refresh.
                  </p>
                </div>
              )}
        </div>
      )}
    </div>
  )
}