Skip to content

Commit 446609b

Browse files
authored
feat: Update disaster types and enhance settings page functionality (#100)
- Expanded available disaster types in the map page to include wildfire, hurricane, tornado, volcano, and heatwave. - Refactored settings page to replace alert types with watched regions, allowing users to add and manage specific regions for alerts. - Implemented region search functionality to enhance user experience when adding watched regions. - Updated user alert preferences to accommodate the new watched regions feature, improving alert customization.
1 parent af38e48 commit 446609b

File tree

16 files changed

+1153
-343
lines changed

16 files changed

+1153
-343
lines changed

client/app/dashboard/map/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export default function MapPage() {
8787
);
8888
};
8989

90-
const availableDisasterTypes = ["earthquake", "flood", "fire", "storm", "tsunami", "other"];
90+
const availableDisasterTypes = ["earthquake", "flood", "wildfire", "hurricane", "tornado", "tsunami", "volcano", "heatwave"];
9191
const hasActiveFilters = countryFilter !== "" || disasterTypeFilters.length > 0 || locationFilter !== "all" || severityFilter !== "all";
9292

9393
const availableCountries = useMemo(() => {

client/app/dashboard/settings/page.tsx

Lines changed: 105 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ import { Switch } from "@/components/ui/switch"
99
import { Button } from "@/components/ui/button"
1010
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
1111
import { Skeleton } from "@/components/ui/skeleton"
12-
import { Checkbox } from "@/components/ui/checkbox"
1312
import {
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"
2120
import { apiClient } from "@/lib/api-client"
2221
import { MapStyleSettings } from "@/components/map-style-settings"
2322
import {
@@ -30,19 +29,29 @@ import {
3029
} from "@/components/ui/dialog"
3130
import { 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+
3345
export 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&apos;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&apos;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>

client/app/onboarding/page.tsx

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { MapPin, CheckCircle2, Loader2, Search, Globe, Bell, AlertCircle, Chevro
66
import { Button } from '@/components/ui/button';
77
import { Card } from '@/components/ui/card';
88
import { Input } from '@/components/ui/input';
9-
import { Checkbox } from '@/components/ui/checkbox';
109
import { Label } from '@/components/ui/label';
1110
import {
1211
Select,
@@ -40,7 +39,6 @@ function OnboardingPageContent() {
4039

4140
// Alert preferences state
4241
const [minSeverity, setMinSeverity] = useState(3);
43-
const [alertTypes, setAlertTypes] = useState(['new_crisis', 'severity_change', 'update']);
4442
const [savingAlerts, setSavingAlerts] = useState(false);
4543

4644
const router = useRouter();
@@ -191,12 +189,6 @@ function OnboardingPageContent() {
191189
}
192190
};
193191

194-
const toggleAlertType = (type: string) => {
195-
setAlertTypes(prev =>
196-
prev.includes(type) ? prev.filter(t => t !== type) : [...prev, type]
197-
);
198-
};
199-
200192
const saveAlertPreferences = async () => {
201193
if (!user?.user_id) return;
202194

@@ -206,10 +198,7 @@ function OnboardingPageContent() {
206198
method: 'PUT',
207199
body: JSON.stringify({
208200
min_severity: minSeverity,
209-
email_min_severity: 3,
210-
alert_types: alertTypes,
211-
regions: null,
212-
disaster_types: null,
201+
watched_regions: null,
213202
email_enabled: true,
214203
}),
215204
});
@@ -448,36 +437,10 @@ function OnboardingPageContent() {
448437
</p>
449438
</div>
450439

451-
<div>
452-
<Label className="text-base font-semibold mb-3 block">Alert Types</Label>
453-
<div className="space-y-3">
454-
{[
455-
{ value: 'new_crisis', label: 'New Crisis', desc: 'When a new disaster is detected' },
456-
{ value: 'severity_change', label: 'Severity Changes', desc: 'When disaster severity increases' },
457-
{ value: 'update', label: 'Updates', desc: 'General updates about ongoing crises' }
458-
].map(type => (
459-
<div key={type.value} className="flex items-start space-x-3 p-3 rounded-lg border hover:bg-muted/50 transition-colors">
460-
<Checkbox
461-
id={type.value}
462-
checked={alertTypes.includes(type.value)}
463-
onCheckedChange={() => toggleAlertType(type.value)}
464-
className="mt-1"
465-
/>
466-
<div className="flex-1">
467-
<Label htmlFor={type.value} className="font-medium cursor-pointer text-sm">
468-
{type.label}
469-
</Label>
470-
<p className="text-xs text-muted-foreground mt-0.5">{type.desc}</p>
471-
</div>
472-
</div>
473-
))}
474-
</div>
475-
</div>
476-
477440
<div className="pt-2 space-y-3">
478441
<Button
479442
onClick={saveAlertPreferences}
480-
disabled={savingAlerts || alertTypes.length === 0}
443+
disabled={savingAlerts}
481444
size="lg"
482445
className="w-full h-12 text-base"
483446
>

0 commit comments

Comments
 (0)