cropguard-system / frontend /src /CropGuard.jsx
The-Bricklayer7's picture
Upload 1069 files
403f212 verified
Raw
History Blame Contribute Delete
6.06 kB
import React, { useState, useRef } from "react";
/**
* CropGuard.jsx — React frontend for the crop disease detection system.
* Implements the four-step farmer flow from §3.10.4:
* home -> preview -> loading -> result
* Talks to the FastAPI backend's POST /predict endpoint.
*
* Set the API base URL via Vite env: VITE_API_URL=http://localhost:8000
*/
const API = import.meta.env.VITE_API_URL || "http://localhost:8000";
const SEVERITY = {
early: { label: "Early stage", urgency: "Routine", color: "#3fa34d",
desc: "Symptoms are small and localised. You have time, but act soon." },
moderate: { label: "Moderate stage", urgency: "Urgent", color: "#e9a625",
desc: "The disease covers a good part of the leaf and may spread fast. Treat this week." },
severe: { label: "Severe stage", urgency: "Emergency", color: "#cf3b2f",
desc: "Most of the leaf or plant is affected. Act today to save the rest of your crop." },
};
export default function CropGuard() {
const [screen, setScreen] = useState("home"); // home | preview | loading | result
const [imgUrl, setImgUrl] = useState(null);
const [file, setFile] = useState(null);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const camRef = useRef(null);
const galRef = useRef(null);
function pick(e) {
const f = e.target.files?.[0];
if (!f) return;
setFile(f);
setImgUrl(URL.createObjectURL(f));
setScreen("preview");
e.target.value = "";
}
async function analyse() {
setScreen("loading");
setError(null);
try {
const fd = new FormData();
fd.append("file", file);
const res = await fetch(`${API}/predict`, { method: "POST", body: fd });
if (!res.ok) throw new Error("Server error");
setResult(await res.json());
setScreen("result");
} catch (err) {
setError("Could not reach the analysis server. Check your connection and try again.");
setScreen("preview");
}
}
function reset() { setScreen("home"); setResult(null); setImgUrl(null); setFile(null); }
return (
<div className="cg">
<header className="cg-head">
<div className="cg-logo">🌿</div>
<div><h1>CropGuard GH</h1><span>Snap a leaf. Know the disease.</span></div>
</header>
{screen === "home" && (
<section>
<div className="cg-hero">
<h2>Diagnose crop disease in seconds</h2>
<p>Photograph a sick leaf and get the disease, its severity, and what to do — free.</p>
</div>
{error && <p className="cg-err">{error}</p>}
<button className="cg-cta" onClick={() => camRef.current.click()}>📷 Take a photo of a leaf</button>
<button className="cg-ghost" onClick={() => galRef.current.click()}>🖼️ Choose from gallery</button>
<p className="cg-crops">Detects: Maize · Tomato · Cassava</p>
</section>
)}
{screen === "preview" && (
<section>
<img className="cg-preview" src={imgUrl} alt="leaf" />
<h3>Is this the right photo?</h3>
<p>Make sure the diseased leaf fills the frame and is in focus.</p>
{error && <p className="cg-err">{error}</p>}
<div className="cg-row">
<button className="cg-ghost" onClick={reset}>← Retake</button>
<button className="cg-cta" onClick={analyse}>🔍 Analyse crop</button>
</div>
</section>
)}
{screen === "loading" && (
<section className="cg-loading">
<div className="cg-spinner" />
<h3>Analysing the leaf…</h3>
<p>Checking colour, spots and damage</p>
</section>
)}
{screen === "result" && result && (
<Result result={result} onReset={reset} />
)}
<input ref={camRef} type="file" accept="image/*" capture="environment" hidden onChange={pick} />
<input ref={galRef} type="file" accept="image/*" hidden onChange={pick} />
</div>
);
}
function Result({ result, onReset }) {
const d = result.disease || {};
const healthy = !!d.healthy;
const sev = !healthy && result.severity ? SEVERITY[result.severity] : null;
return (
<section>
<div className={`cg-rhero ${healthy ? "ok" : "bad"}`}>
<div className="cg-rlabel">Detected disease</div>
<div className="cg-rname">{d.name}</div>
<div className="cg-rcrop">🌿 {d.crop}</div>
<div className="cg-conf">
<span>Confidence</span><b>{Math.round(result.confidence * 100)}%</b>
</div>
</div>
{!healthy && sev && (
<div className="cg-badges">
<div className="cg-badge">
<small>Severity</small>
<b style={{ color: sev.color }}>● {sev.label}</b>
</div>
<div className="cg-badge">
<small>Urgency</small>
<b style={{ color: sev.color }}>{sev.urgency}</b>
</div>
</div>
)}
{sev && <p className="cg-sevdesc">{sev.desc}</p>}
{!healthy && (
<div className="cg-card">
<h4>✅ What to do now</h4>
<ol>{(d.treatment || []).map((t, i) => <li key={i}>{t}</li>)}</ol>
<div className="cg-products">
{(d.products || []).map((p, i) => <span key={i}>🧪 {p}</span>)}
</div>
</div>
)}
{!healthy && (
<div className="cg-card">
<h4>ℹ️ About this disease</h4>
<p>{d.cause}</p>
</div>
)}
{healthy && (
<div className="cg-card cg-ok">
<h4>✅ No disease detected</h4>
<p>This leaf looks healthy. Keep monitoring your field weekly.</p>
</div>
)}
<p className="cg-disc">
⚠️ This is a diagnostic aid, not a replacement for an extension officer.
For unusual or severe cases, consult MoFA.
</p>
<button className="cg-cta" onClick={onReset}>🍃 Scan another leaf</button>
</section>
);
}