185 lines
7.2 KiB
TypeScript
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
|