cropintel / components /OutbreakMap.tsx
Jaithra Polavarapu
feat(hf): deploy the full redesigned frontend (keep in-process inference)
965ef8f
Raw
History Blame Contribute Delete
12.1 kB
'use client'
import { useState, useEffect } from 'react'
import { WorldMap } from '@/components/ui/world-map'
import { MapPin, AlertTriangle, X, Save } from 'lucide-react'
import { motion } from 'framer-motion'
interface OutbreakReport {
id: string
lat: number
lng: number
crop: string
disease: string
severity: 'low' | 'medium' | 'high'
date: string
description: string
}
export default function OutbreakMap() {
const [reports, setReports] = useState<OutbreakReport[]>([])
const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number } | null>(null)
const [showReportForm, setShowReportForm] = useState(false)
const [formData, setFormData] = useState({
crop: '',
disease: '',
severity: 'medium' as 'low' | 'medium' | 'high',
description: '',
})
// Convert reports to map dots format
const mapDots = reports.map((report) => ({
start: { lat: report.lat, lng: report.lng, label: report.disease },
end: { lat: report.lat, lng: report.lng, label: report.disease },
}))
const handleMapClick = (e: React.MouseEvent<HTMLDivElement>) => {
// Find the map container
const mapContainer = e.currentTarget.querySelector('[class*="aspect-[2/1]"]')
if (!mapContainer) return
const rect = mapContainer.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// Convert pixel coordinates to lat/lng (map is 800x400 viewBox)
const lng = (x / rect.width) * 360 - 180
const lat = 90 - (y / rect.height) * 180
// Clamp values to valid ranges
const clampedLat = Math.max(-90, Math.min(90, lat))
const clampedLng = Math.max(-180, Math.min(180, lng))
setSelectedLocation({ lat: clampedLat, lng: clampedLng })
setShowReportForm(true)
}
const handleSubmitReport = () => {
if (!selectedLocation || !formData.crop || !formData.disease) {
alert('Please add the crop and what the issue looks like.')
return
}
const newReport: OutbreakReport = {
id: Date.now().toString(),
lat: selectedLocation.lat,
lng: selectedLocation.lng,
crop: formData.crop,
disease: formData.disease,
severity: formData.severity,
date: new Date().toISOString(),
description: formData.description,
}
setReports([...reports, newReport])
setShowReportForm(false)
setSelectedLocation(null)
setFormData({
crop: '',
disease: '',
severity: 'medium',
description: '',
})
}
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high':
return 'text-red-600 bg-red-50 border-red-200'
case 'medium':
return 'text-orange-600 bg-orange-50 border-orange-200'
case 'low':
return 'text-yellow-600 bg-yellow-50 border-yellow-200'
default:
return 'text-slate-600 bg-slate-50 border-slate-200'
}
}
return (
<div className="min-h-screen min-h-[100dvh] bg-field-cream px-3 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-4 sm:py-8">
<div className="container mx-auto max-w-7xl">
{/* Header */}
<div className="mb-8 text-center">
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-3">
Local crop issue map
</h1>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Tap the map where you are seeing crop trouble so nearby farms can watch their fields.
</p>
</div>
{/* Map Container */}
<div className="bg-white rounded-xl shadow-lg p-6 mb-6 border border-slate-200">
<div className="relative">
<WorldMap dots={mapDots} lineColor="#ef4444" />
{/* Clickable overlay for map interaction */}
<div
onClick={handleMapClick}
className="absolute inset-0 cursor-crosshair z-10"
style={{ pointerEvents: 'auto' }}
/>
{selectedLocation && (
<div className="absolute top-4 right-4 bg-white rounded-lg shadow-lg p-4 border border-slate-200 z-20">
<p className="text-sm font-semibold text-slate-900 mb-1">Selected Location</p>
<p className="text-xs text-slate-600">
Lat: {selectedLocation.lat.toFixed(4)}, Lng: {selectedLocation.lng.toFixed(4)}
</p>
</div>
)}
<div className="absolute bottom-4 left-4 bg-primary-600 text-white px-4 py-2 rounded-lg shadow-lg z-20">
<p className="text-sm font-semibold flex items-center gap-2">
<MapPin className="w-4 h-4" />
Tap map to report crop trouble
</p>
</div>
</div>
</div>
{/* Report Form Modal — avoid motion opacity on full-screen layer (invisible overlays still capture clicks). */}
{showReportForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div
role="presentation"
className="absolute inset-0"
onClick={() => {
setShowReportForm(false)
setSelectedLocation(null)
}}
/>
<div
role="dialog"
aria-modal="true"
onClick={(e) => e.stopPropagation()}
className="relative z-10 max-h-[90dvh] w-full max-w-md overflow-y-auto overscroll-contain rounded-xl border border-slate-200 bg-white p-6 shadow-2xl"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<AlertTriangle className="w-6 h-6 text-orange-500" />
Report crop trouble
</h2>
<button
onClick={() => {
setShowReportForm(false)
setSelectedLocation(null)
}}
className="text-slate-400 hover:text-slate-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Crop affected *
</label>
<input
type="text"
value={formData.crop}
onChange={(e) => setFormData({ ...formData, crop: e.target.value })}
placeholder="Corn, wheat, rice"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
What it looks like *
</label>
<input
type="text"
value={formData.disease}
onChange={(e) => setFormData({ ...formData, disease: e.target.value })}
placeholder="Rust, blight, yellowing"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
How bad is it? *
</label>
<select
value={formData.severity}
onChange={(e) =>
setFormData({
...formData,
severity: e.target.value as 'low' | 'medium' | 'high',
})
}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Field notes
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="How much of the field is affected? When did you notice it?"
rows={3}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<button
onClick={handleSubmitReport}
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-semibold py-3 px-6 rounded-lg transition-colors flex items-center justify-center gap-2 shadow-lg"
>
<Save className="w-5 h-5" />
Share report
</button>
</div>
</div>
</div>
)}
{/* Reports List */}
<div className="bg-white rounded-xl shadow-lg p-6 border border-slate-200">
<h2 className="text-2xl font-bold text-slate-900 mb-4 flex items-center gap-2">
<MapPin className="w-6 h-6 text-primary-600" />
Shared crop reports ({reports.length})
</h2>
{reports.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<MapPin className="w-12 h-12 mx-auto mb-4 text-slate-300" />
<p>No crop issues reported yet. Tap the map to share what you are seeing.</p>
</div>
) : (
<div className="space-y-4">
{reports.map((report) => (
<motion.div
key={report.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="border border-slate-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-slate-900">
{report.crop} - {report.disease}
</h3>
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${getSeverityColor(
report.severity
)}`}
>
{report.severity.toUpperCase()}
</span>
</div>
<p className="text-sm text-slate-600 mb-2">
Location: {report.lat.toFixed(4)}, {report.lng.toFixed(4)}
</p>
{report.description && (
<p className="text-sm text-slate-700">{report.description}</p>
)}
<p className="text-xs text-slate-500 mt-2">
Reported: {new Date(report.date).toLocaleString()}
</p>
</div>
</div>
</motion.div>
))}
</div>
)}
</div>
</div>
</div>
)
}