ooooeee
This commit is contained in:
247
apps/web/src/app/browse/[plantId]/DiseaseCards.tsx
Normal file
247
apps/web/src/app/browse/[plantId]/DiseaseCards.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import type { Disease, CausalAgentType, Severity } from "@/lib/types";
|
||||
import ImageLightbox from "@/components/ImageLightbox";
|
||||
|
||||
// ─── Severity badge ───
|
||||
|
||||
function SeverityBadge({ severity }: { severity: Severity }) {
|
||||
const colors: Record<Severity, string> = {
|
||||
low: "bg-leaf-green-100 text-leaf-green-800 dark:bg-leaf-green-900/40 dark:text-leaf-green-300",
|
||||
moderate:
|
||||
"bg-warning-amber-100 text-warning-amber-800 dark:bg-warning-amber-900/40 dark:text-warning-amber-300",
|
||||
high: "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300",
|
||||
critical: "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300",
|
||||
};
|
||||
|
||||
const labels: Record<Severity, string> = {
|
||||
low: "Low",
|
||||
moderate: "Moderate",
|
||||
high: "High",
|
||||
critical: "Critical",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[severity]}`}
|
||||
>
|
||||
{severity === "critical" ? "🚨 " : ""}
|
||||
{labels[severity]} Severity
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Disease type badge ───
|
||||
|
||||
function TypeBadge({ type }: { type: CausalAgentType }) {
|
||||
const colors: Record<CausalAgentType, string> = {
|
||||
fungal: "bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300",
|
||||
bacterial: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
|
||||
viral: "bg-pink-100 text-pink-800 dark:bg-pink-900/40 dark:text-pink-300",
|
||||
environmental: "bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[type]}`}
|
||||
>
|
||||
{type === "environmental" ? "Environmental" : type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Disease card ───
|
||||
|
||||
function DiseaseCard({
|
||||
disease,
|
||||
onImageClick,
|
||||
}: {
|
||||
disease: Disease;
|
||||
onImageClick: (disease: Disease) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
id={`disease-${disease.id}`}
|
||||
className="rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 overflow-hidden shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
{/* Card header */}
|
||||
<div className="p-5 sm:p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{disease.name}
|
||||
</h3>
|
||||
{disease.scientificName && (
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 italic mt-0.5">
|
||||
{disease.scientificName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<TypeBadge type={disease.causalAgentType} />
|
||||
<SeverityBadge severity={disease.severity} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disease image or placeholder */}
|
||||
<div className="mb-4 rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700">
|
||||
{disease.imageUrl ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onImageClick(disease)}
|
||||
className="block w-full cursor-pointer group"
|
||||
aria-label={`View larger image of ${disease.name} symptoms`}
|
||||
>
|
||||
<img
|
||||
src={disease.imageUrl}
|
||||
alt={`${disease.name} symptoms`}
|
||||
className="w-full h-48 sm:h-64 object-cover transition-all duration-200 group-hover:brightness-75 group-hover:scale-[1.02]"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-36 sm:h-48 bg-gradient-to-br from-zinc-100 to-zinc-200 dark:from-zinc-800 dark:to-zinc-900">
|
||||
<div className="text-center">
|
||||
<span className="text-5xl block mb-2" aria-hidden="true">
|
||||
{disease.causalAgentType === "fungal"
|
||||
? "🍄"
|
||||
: disease.causalAgentType === "bacterial"
|
||||
? "🦠"
|
||||
: disease.causalAgentType === "viral"
|
||||
? "🧬"
|
||||
: disease.causalAgentType === "environmental"
|
||||
? "🌡️"
|
||||
: "🔬"}
|
||||
</span>
|
||||
<p className="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
{disease.causalAgentType === "fungal"
|
||||
? "Fungal pathogen"
|
||||
: disease.causalAgentType === "bacterial"
|
||||
? "Bacterial infection"
|
||||
: disease.causalAgentType === "viral"
|
||||
? "Viral infection"
|
||||
: "Environmental disorder"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed mb-4">
|
||||
{disease.description}
|
||||
</p>
|
||||
|
||||
{/* Details grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Symptoms */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-red-600 dark:text-red-400 mb-2 flex items-center gap-1">
|
||||
<span aria-hidden="true">⚠️</span> Symptoms
|
||||
</h4>
|
||||
<ul className="space-y-1.5">
|
||||
{disease.symptoms.map((symptom, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-start gap-2 text-sm text-zinc-600 dark:text-zinc-300"
|
||||
>
|
||||
<span className="mt-1 shrink-0 h-1.5 w-1.5 rounded-full bg-red-400 dark:bg-red-500" />
|
||||
{symptom}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Causes */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-orange-600 dark:text-orange-400 mb-2 flex items-center gap-1">
|
||||
<span aria-hidden="true">🔍</span> Causes
|
||||
</h4>
|
||||
<ul className="space-y-1.5">
|
||||
{disease.causes.map((cause, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-start gap-2 text-sm text-zinc-600 dark:text-zinc-300"
|
||||
>
|
||||
<span className="mt-1 shrink-0 h-1.5 w-1.5 rounded-full bg-orange-400 dark:bg-orange-500" />
|
||||
{cause}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Treatment Steps */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-leaf-green-600 dark:text-leaf-green-400 mb-2 flex items-center gap-1">
|
||||
<span aria-hidden="true">💊</span> Treatment Steps
|
||||
</h4>
|
||||
<ol className="space-y-1.5 list-decimal list-inside">
|
||||
{disease.treatment.map((step, i) => (
|
||||
<li key={i} className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{step}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Prevention Tips */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-leaf-green-600 dark:text-leaf-green-400 mb-2 flex items-center gap-1">
|
||||
<span aria-hidden="true">🛡️</span> Prevention Tips
|
||||
</h4>
|
||||
<ul className="space-y-1.5">
|
||||
{disease.prevention.map((tip, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-start gap-2 text-sm text-zinc-600 dark:text-zinc-300"
|
||||
>
|
||||
<span className="mt-1 shrink-0 h-1.5 w-1.5 rounded-full bg-leaf-green-400 dark:bg-leaf-green-500" />
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Client component wrapper ───
|
||||
|
||||
export default function DiseaseCards({ diseases }: { diseases: Disease[] }) {
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false);
|
||||
const [lightboxIndex, setLightboxIndex] = useState(0);
|
||||
|
||||
// Build list of images from diseases that have imageUrls
|
||||
const images = diseases
|
||||
.filter((d) => d.imageUrl)
|
||||
.map((d) => ({ src: d.imageUrl!, alt: `${d.name} symptoms` }));
|
||||
|
||||
const handleImageClick = useCallback(
|
||||
(disease: Disease) => {
|
||||
const index = images.findIndex((img) => img.src === disease.imageUrl);
|
||||
setLightboxIndex(index >= 0 ? index : 0);
|
||||
setLightboxOpen(true);
|
||||
},
|
||||
[images],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => setLightboxOpen(false), []);
|
||||
|
||||
if (diseases.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
{diseases.map((disease) => (
|
||||
<DiseaseCard key={disease.id} disease={disease} onImageClick={handleImageClick} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{lightboxOpen && images.length > 0 && (
|
||||
<ImageLightbox images={images} initialIndex={lightboxIndex} onClose={handleClose} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user