geocrop-platform./apps/web/src/App.tsx

185 lines
7.2 KiB
TypeScript

import { useState } from 'react'
import MapComponent from './MapComponent'
import JobForm from './JobForm'
import StatusMonitor from './StatusMonitor'
import Welcome from './Welcome'
import Login from './Login'
import Admin from './Admin'
import TechnicalDocs from './TechnicalDocs'
type ViewState = 'welcome' | 'login' | 'app' | 'admin' | 'portfolio'
// App entry point for GeoCrop Web
function App() {
const [view, setView] = useState<ViewState>('welcome')
const [isAdmin, setIsAdmin] = useState<boolean>(localStorage.getItem('isAdmin') === 'true')
const [token, setToken] = useState<string | null>(localStorage.getItem('token'))
const [jobs, setJobs] = useState<string[]>([])
const [selectedCoords, setSelectedCoords] = useState<{lat: string, lon: string} | null>(null)
const [finishedJobs, setFinishedJobs] = useState<Record<string, any>>({})
const [activeResultUrl, setActiveResultUrl] = useState<string | undefined>(undefined)
const [activeROI, setActiveROI] = useState<{lat: number, lon: number, radius_m: number} | undefined>(undefined)
const handleWelcomeContinue = () => {
if (token) {
setView('app')
} else {
setView('login')
}
}
const handleViewPortfolio = () => {
setView('portfolio')
}
const handleLoginSuccess = (newToken: string, isUserAdmin: boolean) => {
localStorage.setItem('token', newToken)
localStorage.setItem('isAdmin', isUserAdmin ? 'true' : 'false')
setToken(newToken)
setIsAdmin(isUserAdmin)
setView('app')
}
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('isAdmin')
setToken(null)
setIsAdmin(false)
setView('welcome')
}
const handleJobSubmitted = (jobId: string) => {
setJobs(prev => [...prev, jobId])
}
const handleCoordsSelected = (lat: number, lon: number) => {
setSelectedCoords({ lat: lat.toFixed(6), lon: lon.toFixed(6) })
}
const handleJobFinished = (jobId: string, data: any) => {
setFinishedJobs(prev => ({ ...prev, [jobId]: data.result }))
// Auto-overlay if it's the latest finished job
if (data.result && (data.result.refined_url || data.result.refined_geotiff)) {
setActiveResultUrl(data.result.refined_url || data.result.refined_geotiff)
setActiveROI(data.roi)
}
}
if (view === 'welcome') {
return <div style={{ minHeight: '100vh', background: '#f0f2f5', display: 'flex', alignItems: 'center' }}>
<Welcome onContinue={handleWelcomeContinue} onViewPortfolio={handleViewPortfolio} />
</div>
}
if (view === 'portfolio') {
return <div style={{ minHeight: '100vh', background: '#f0f2f5', display: 'flex', alignItems: 'center' }}>
<TechnicalDocs onBack={() => setView('welcome')} />
</div>
}
if (view === 'login') {
return <div style={{ minHeight: '100vh', background: '#f0f2f5', display: 'flex', alignItems: 'center' }}>
<Login onLoginSuccess={handleLoginSuccess} />
</div>
}
if (view === 'admin') {
return (
<div style={{ minHeight: '100vh', background: '#f0f2f5' }}>
<nav style={{ background: '#333', color: 'white', padding: '10px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 'bold' }}>GeoCrop Admin</span>
<div>
<button onClick={() => setView('app')} style={{ background: '#555', color: 'white', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer', marginRight: '10px' }}>Back to Map</button>
<button onClick={handleLogout} style={{ background: '#dc3545', color: 'white', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Logout</button>
</div>
</nav>
<Admin />
</div>
)
}
return (
<div style={{ width: '100vw', height: '100vh', margin: 0, padding: 0, overflow: 'hidden' }}>
<MapComponent
onCoordsSelected={handleCoordsSelected}
resultUrl={activeResultUrl}
roi={activeROI}
/>
<div style={{
position: 'absolute',
top: '20px',
left: '20px',
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 15px rgba(0,0,0,0.3)',
zIndex: 1000,
width: '320px',
maxHeight: 'calc(100vh - 40px)',
overflowY: 'auto',
fontFamily: 'system-ui, -apple-system, sans-serif'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 'bold', color: '#333' }}>GeoCrop</h1>
<p style={{ margin: '5px 0 15px', color: '#666', fontSize: '14px' }}>Crop Classification Zimbabwe</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<button
onClick={handleLogout}
style={{ background: 'none', border: 'none', color: '#dc3545', cursor: 'pointer', fontSize: '11px', fontWeight: 'bold', padding: '2px' }}
>
Logout
</button>
{isAdmin && (
<button
onClick={() => setView('admin')}
style={{ background: '#1a73e8', border: 'none', color: 'white', cursor: 'pointer', fontSize: '10px', fontWeight: 'bold', padding: '4px 8px', borderRadius: '4px' }}
>
Admin Panel
</button>
)}
</div>
</div>
<div style={{ marginBottom: '15px', padding: '10px', background: '#f8f9fa', borderRadius: '4px', border: '1px solid #e9ecef' }}>
<p style={{ margin: 0, fontSize: '11px', fontWeight: 'bold', color: '#6c757d', textTransform: 'uppercase' }}>Current View:</p>
<p style={{ margin: '2px 0 0', fontSize: '14px', color: '#212529', fontWeight: '500' }}>Classification (2021-2022)</p>
<p style={{ margin: '8px 0 0', fontSize: '11px', color: '#0066cc', fontStyle: 'italic' }}>Tip: Click map to set coordinates</p>
</div>
<JobForm
onJobSubmitted={handleJobSubmitted}
selectedLat={selectedCoords?.lat}
selectedLon={selectedCoords?.lon}
/>
{jobs.length > 0 && (
<div style={{ marginTop: '20px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
<h2 style={{ fontSize: '16px', margin: '0 0 10px', fontWeight: 'bold' }}>Job History</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{jobs.map(id => (
<StatusMonitor
key={id}
jobId={id}
onJobFinished={handleJobFinished}
/>
))}
</div>
</div>
)}
{Object.keys(finishedJobs).length > 0 && (
<div style={{ marginTop: '20px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
<h3 style={{ fontSize: '14px', margin: '0 0 10px', fontWeight: 'bold', color: '#28a745' }}>Completed Results</h3>
<p style={{ fontSize: '11px', color: '#666' }}>Predicted maps are being uploaded to the tiler. Check result URLs in the browser console for direct access.</p>
</div>
)}
</div>
</div>
)
}
export default App