@@ -9,15 +9,14 @@ import { Switch } from "@/components/ui/switch"
99import { Button } from "@/components/ui/button"
1010import { Avatar , AvatarFallback , AvatarImage } from "@/components/ui/avatar"
1111import { Skeleton } from "@/components/ui/skeleton"
12- import { Checkbox } from "@/components/ui/checkbox"
1312import {
1413 Select ,
1514 SelectContent ,
1615 SelectItem ,
1716 SelectTrigger ,
1817 SelectValue ,
1918} from "@/components/ui/select"
20- import { Search , Check , AlertCircle , Loader2 , RefreshCw , Trash2 , MapPin , Calendar , User , Edit2 , X } from "lucide-react"
19+ import { Search , Check , AlertCircle , Loader2 , RefreshCw , Trash2 , MapPin , Calendar , User , Edit2 , X , Plus } from "lucide-react"
2120import { apiClient } from "@/lib/api-client"
2221import { MapStyleSettings } from "@/components/map-style-settings"
2322import {
@@ -30,19 +29,29 @@ import {
3029} from "@/components/ui/dialog"
3130import { useRouter } from "next/navigation"
3231
32+ interface WatchedRegion {
33+ name : string
34+ lat : number
35+ lng : number
36+ bounds ?: {
37+ ne_lat : number
38+ ne_lng : number
39+ sw_lat : number
40+ sw_lng : number
41+ }
42+ place_id ?: string
43+ }
44+
3345export default function SettingsPage ( ) {
3446 const { user, loading, refreshAuth } = useAuth ( )
3547 const router = useRouter ( )
3648 const [ emailNotifications , setEmailNotifications ] = useState ( false )
37- const [ twoFactor , setTwoFactor ] = useState ( false )
38- const [ autoUpdates , setAutoUpdates ] = useState ( false )
39- const [ shareUsageData , setShareUsageData ] = useState ( true )
4049
4150 // Alert preferences
4251 const [ minSeverity , setMinSeverity ] = useState ( 3 )
43- const [ emailMinSeverity , setEmailMinSeverity ] = useState ( 3 )
44- const [ alertTypes , setAlertTypes ] = useState ( [ 'new_crisis' , 'severity_change' , 'update' ] )
45- const [ regions , setRegions ] = useState ( '' )
52+ const [ watchedRegions , setWatchedRegions ] = useState < WatchedRegion [ ] > ( [ ] )
53+ const [ regionSearch , setRegionSearch ] = useState ( '' )
54+ const [ searchingRegion , setSearchingRegion ] = useState ( false )
4655 const [ saving , setSaving ] = useState ( false )
4756 const [ saveSuccess , setSaveSuccess ] = useState ( false )
4857 const [ updatingLocation , setUpdatingLocation ] = useState ( false )
@@ -58,9 +67,7 @@ export default function SettingsPage() {
5867 if ( response . ok ) {
5968 const data = await response . json ( )
6069 setMinSeverity ( data . min_severity || 3 )
61- setEmailMinSeverity ( data . email_min_severity || 3 )
62- setAlertTypes ( data . alert_types || [ 'new_crisis' , 'severity_change' , 'update' ] )
63- setRegions ( data . regions ?. join ( ', ' ) || '' )
70+ setWatchedRegions ( data . watched_regions || [ ] )
6471 setEmailNotifications ( data . email_enabled || false )
6572 }
6673 } catch ( err ) {
@@ -84,9 +91,7 @@ export default function SettingsPage() {
8491 method : 'PUT' ,
8592 body : JSON . stringify ( {
8693 min_severity : minSeverity ,
87- email_min_severity : emailMinSeverity ,
88- alert_types : alertTypes ,
89- regions : regions ? regions . split ( ',' ) . map ( r => r . trim ( ) ) . filter ( r => r ) : null ,
94+ watched_regions : watchedRegions . length > 0 ? watchedRegions : null ,
9095 email_enabled : overrides ?. email_enabled ?? emailNotifications ,
9196 } ) ,
9297 } )
@@ -102,10 +107,31 @@ export default function SettingsPage() {
102107 }
103108 }
104109
105- const toggleAlertType = ( type : string ) => {
106- setAlertTypes ( prev =>
107- prev . includes ( type ) ? prev . filter ( t => t !== type ) : [ ...prev , type ]
108- )
110+ const searchAndAddRegion = async ( ) => {
111+ if ( ! regionSearch . trim ( ) ) return
112+
113+ setSearchingRegion ( true )
114+ try {
115+ const response = await apiClient ( `/api/alerts/regions/search?query=${ encodeURIComponent ( regionSearch ) } ` )
116+ if ( response . ok ) {
117+ const region : WatchedRegion = await response . json ( )
118+ if ( ! watchedRegions . some ( r => r . place_id === region . place_id ) ) {
119+ setWatchedRegions ( [ ...watchedRegions , region ] )
120+ }
121+ setRegionSearch ( '' )
122+ } else {
123+ alert ( 'Region not found. Try a different search term.' )
124+ }
125+ } catch ( err ) {
126+ console . error ( 'Failed to search region:' , err )
127+ alert ( 'Failed to search region' )
128+ } finally {
129+ setSearchingRegion ( false )
130+ }
131+ }
132+
133+ const removeRegion = ( index : number ) => {
134+ setWatchedRegions ( watchedRegions . filter ( ( _ , i ) => i !== index ) )
109135 }
110136
111137 const updateLocation = ( ) => {
@@ -510,19 +536,19 @@ export default function SettingsPage() {
510536 < div className = "flex items-start gap-3" >
511537 < AlertCircle className = "w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
512538 < div className = "text-sm space-y-2" >
513- < p className = "font-semibold text-foreground" > How notifications work</ p >
539+ < p className = "font-semibold text-foreground" > How alerts work</ p >
514540 < div className = "space-y-1.5 text-muted-foreground" >
515- < p > • < strong > Location-based:</ strong > You'll receive alerts for disasters within 100km of your location</ p >
516- < p > • < strong > Severity filter:</ strong > Only alerts at or above your minimum severity will appear </ p >
517- < p > • < strong > Alert types :</ strong > Choose which types of alerts you want to see </ p >
518- < p > • < strong > Regions :</ strong > Optionally specify regions to receive alerts from anywhere </ p >
541+ < p > • < strong > Location-based:</ strong > Alerts for disasters within 100km of your location</ p >
542+ < p > • < strong > Severity filter:</ strong > Only alerts at or above your minimum severity</ p >
543+ < p > • < strong > Custom regions :</ strong > Optionally add regions to get alerts from anywhere </ p >
544+ < p > • < strong > No location :</ strong > Receive all global alerts</ p >
519545 </ div >
520546 </ div >
521547 </ div >
522548 </ div >
523549
524550 < div className = "space-y-1" >
525- < Label className = "text-base font-semibold mb-3 block" > Step 1: Dashboard Alert Severity</ Label >
551+ < Label className = "text-base font-semibold mb-3 block" > Minimum Severity</ Label >
526552 < Select value = { String ( minSeverity ) } onValueChange = { ( value ) => setMinSeverity ( Number ( value ) ) } >
527553 < SelectTrigger className = "w-full" >
528554 < SelectValue />
@@ -536,82 +562,73 @@ export default function SettingsPage() {
536562 </ SelectContent >
537563 </ Select >
538564 < p className = "text-xs text-muted-foreground mt-2" >
539- Only alerts at this severity level or higher will appear in your dashboard. Lower severity = more alerts .
565+ Only disasters at this severity or higher will trigger alerts (both dashboard and email) .
540566 </ p >
541567 </ div >
542568
543- < div className = "space-y-1" >
544- < Label className = "text-base font-semibold mb-3 block" > Step 2: Email Alert Severity</ Label >
545- < Select value = { String ( emailMinSeverity ) } onValueChange = { ( value ) => setEmailMinSeverity ( Number ( value ) ) } >
546- < SelectTrigger className = "w-full" >
547- < SelectValue />
548- </ SelectTrigger >
549- < SelectContent >
550- < SelectItem value = "1" > Low (1) - All alerts</ SelectItem >
551- < SelectItem value = "2" > Medium (2) - Moderate and above</ SelectItem >
552- < SelectItem value = "3" > High (3) - Serious alerts only</ SelectItem >
553- < SelectItem value = "4" > Very High (4) - Critical only</ SelectItem >
554- < SelectItem value = "5" > Critical (5) - Most severe only</ SelectItem >
555- </ SelectContent >
556- </ Select >
557- < p className = "text-xs text-muted-foreground mt-2" >
558- Only emails will be sent for alerts at this severity level or higher. This is separate from dashboard alerts.
569+ < div className = "space-y-3" >
570+ < Label className = "text-base font-semibold block" >
571+ Watch Additional Regions (Optional)
572+ </ Label >
573+ < p className = "text-xs text-muted-foreground" >
574+ Get alerts from these regions even if they're outside your 100km radius.
559575 </ p >
560- </ div >
576+
577+ < div className = "flex gap-2" >
578+ < Input
579+ placeholder = "Search for a region (e.g., California, Texas)"
580+ value = { regionSearch }
581+ onChange = { ( e ) => setRegionSearch ( e . target . value ) }
582+ onKeyDown = { ( e ) => {
583+ if ( e . key === 'Enter' ) {
584+ e . preventDefault ( )
585+ void searchAndAddRegion ( )
586+ }
587+ } }
588+ className = "text-sm"
589+ />
590+ < Button
591+ onClick = { searchAndAddRegion }
592+ disabled = { searchingRegion || ! regionSearch . trim ( ) }
593+ size = "sm"
594+ >
595+ { searchingRegion ? (
596+ < Loader2 className = "w-4 h-4 animate-spin" />
597+ ) : (
598+ < Plus className = "w-4 h-4" />
599+ ) }
600+ </ Button >
601+ </ div >
561602
562- < div className = "space-y-1" >
563- < Label className = "text-base font-semibold mb-3 block" > Step 3: Alert Types</ Label >
564- < p className = "text-xs text-muted-foreground mb-3" > Select which types of alerts you want to receive:</ p >
565- < div className = "space-y-3" >
566- { [
567- { value : 'new_crisis' , label : 'New Crisis' , desc : 'When a new disaster is first detected' } ,
568- { value : 'severity_change' , label : 'Severity Changes' , desc : 'When an existing disaster becomes more severe' } ,
569- { value : 'update' , label : 'Updates' , desc : 'General updates about ongoing crises' }
570- ] . map ( type => (
571- < div key = { type . value } className = "flex items-start space-x-3 p-3 rounded-lg border hover:bg-muted/50 transition-colors" >
572- < Checkbox
573- id = { type . value }
574- checked = { alertTypes . includes ( type . value ) }
575- onCheckedChange = { ( ) => toggleAlertType ( type . value ) }
576- className = "mt-1"
577- />
578- < div className = "flex-1" >
579- < Label htmlFor = { type . value } className = "font-medium cursor-pointer text-sm" >
580- { type . label }
581- </ Label >
582- < p className = "text-xs text-muted-foreground mt-0.5" > { type . desc } </ p >
603+ { watchedRegions . length > 0 && (
604+ < div className = "space-y-2" >
605+ { watchedRegions . map ( ( region , index ) => (
606+ < div
607+ key = { region . place_id || index }
608+ className = "flex items-center justify-between p-2 rounded-lg border bg-muted/30"
609+ >
610+ < div className = "flex items-center gap-2" >
611+ < MapPin className = "w-4 h-4 text-muted-foreground" />
612+ < span className = "text-sm" > { region . name } </ span >
613+ </ div >
614+ < Button
615+ variant = "ghost"
616+ size = "sm"
617+ onClick = { ( ) => removeRegion ( index ) }
618+ className = "h-6 w-6 p-0"
619+ >
620+ < X className = "w-4 h-4" />
621+ </ Button >
583622 </ div >
584- </ div >
585- ) ) }
586- </ div >
587- { alertTypes . length === 0 && (
588- < p className = "text-xs text-red-600 dark:text-red-400 mt-2" >
589- ⚠️ You must select at least one alert type
590- </ p >
623+ ) ) }
624+ </ div >
591625 ) }
592626 </ div >
593627
594- < div className = "space-y-1" >
595- < Label htmlFor = "regions" className = "text-base font-semibold mb-2 block" >
596- Step 4: Regions (Optional)
597- </ Label >
598- < Input
599- id = "regions"
600- placeholder = "e.g., Texas, California, New York"
601- value = { regions }
602- onChange = { ( e ) => setRegions ( e . target . value ) }
603- className = "text-sm"
604- />
605- < p className = "text-xs text-muted-foreground mt-2" >
606- < strong > Optional:</ strong > Enter specific regions (comma-separated) to receive alerts from anywhere, even outside your 100km radius.
607- Leave empty to only receive alerts based on your location radius.
608- </ p >
609- </ div >
610-
611628 < div className = "pt-2 border-t mt-auto" >
612629 < Button
613630 onClick = { ( ) => savePreferences ( ) }
614- disabled = { saving || alertTypes . length === 0 }
631+ disabled = { saving }
615632 className = "w-full"
616633 >
617634 { saving ? (
@@ -628,11 +645,6 @@ export default function SettingsPage() {
628645 'Save Preferences'
629646 ) }
630647 </ Button >
631- { alertTypes . length === 0 && (
632- < p className = "text-xs text-red-600 dark:text-red-400 mt-2 text-center" >
633- Please select at least one alert type to save
634- </ p >
635- ) }
636648 </ div >
637649 </ CardContent >
638650 </ Card >
0 commit comments