search, db integration
This commit is contained in:
@@ -1,44 +1,50 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPlantById, type Disease } from "@/data/plants";
|
||||
import type { Metadata } from "next";
|
||||
import { getPlantWithDiseases } from "@/lib/api/diseases-db";
|
||||
import { getEmojiForCategory, getPlantDescription } from "@/lib/display-helpers";
|
||||
import type { Disease, CausalAgentType, Severity } from "@/lib/types";
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ plantId: string }>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { plants } = await import("@/data/plants");
|
||||
return plants.map((plant) => ({
|
||||
plantId: plant.id,
|
||||
const { getDb } = await import("@/lib/db/index");
|
||||
const { plants } = await import("@/lib/db/schema");
|
||||
const db = getDb();
|
||||
const rows = await db.select({ id: plants.id }).from(plants);
|
||||
return rows.map((p: { id: string }) => ({
|
||||
plantId: p.id,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { plantId } = await params;
|
||||
const plant = getPlantById(plantId);
|
||||
const result = await getPlantWithDiseases(plantId);
|
||||
|
||||
if (!plant) {
|
||||
if (!result) {
|
||||
return { title: "Plant Not Found" };
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${plant.commonName} — Diseases & Care`,
|
||||
description: `Learn about ${plant.commonName} (${plant.scientificName}) diseases, symptoms, causes, and treatments. ${plant.diseases.length} diseases documented.`,
|
||||
title: `${result.plant.commonName} — Diseases & Care`,
|
||||
description: `Learn about ${result.plant.commonName} (${result.plant.scientificName}) diseases, symptoms, causes, and treatments. ${result.diseases.length} diseases documented.`,
|
||||
};
|
||||
}
|
||||
|
||||
/* ─── Severity badge ─── */
|
||||
function SeverityBadge({ severity }: { severity: Disease["severity"] }) {
|
||||
const colors: Record<Disease["severity"], string> = {
|
||||
// ─── 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",
|
||||
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<Disease["severity"], string> = {
|
||||
const labels: Record<Severity, string> = {
|
||||
low: "Low",
|
||||
moderate: "Moderate",
|
||||
high: "High",
|
||||
@@ -55,26 +61,27 @@ function SeverityBadge({ severity }: { severity: Disease["severity"] }) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Disease type badge ─── */
|
||||
function TypeBadge({ type }: { type: Disease["type"] }) {
|
||||
const colors: Record<Disease["type"], string> = {
|
||||
// ─── 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",
|
||||
pest: "bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300",
|
||||
physiological: "bg-zinc-100 text-zinc-800 dark:bg-zinc-700 dark:text-zinc-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.charAt(0).toUpperCase() + type.slice(1)}
|
||||
{type === "environmental" ? "Environmental" : type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Disease card (expandable) ─── */
|
||||
// ─── Disease card ───
|
||||
|
||||
function DiseaseCard({ disease }: { disease: Disease }) {
|
||||
return (
|
||||
<div
|
||||
@@ -95,11 +102,23 @@ function DiseaseCard({ disease }: { disease: Disease }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<TypeBadge type={disease.type} />
|
||||
<TypeBadge type={disease.causalAgentType} />
|
||||
<SeverityBadge severity={disease.severity} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disease image */}
|
||||
{disease.imageUrl && (
|
||||
<div className="mb-4 rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700">
|
||||
<img
|
||||
src={disease.imageUrl}
|
||||
alt={`${disease.name} symptoms on ${disease.plantId}`}
|
||||
className="w-full h-48 sm:h-64 object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed mb-4">
|
||||
{disease.description}
|
||||
</p>
|
||||
@@ -148,11 +167,8 @@ function DiseaseCard({ disease }: { disease: Disease }) {
|
||||
<span aria-hidden="true">💊</span> Treatment Steps
|
||||
</h4>
|
||||
<ol className="space-y-1.5 list-decimal list-inside">
|
||||
{disease.treatmentSteps.map((step, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="text-sm text-zinc-600 dark:text-zinc-300"
|
||||
>
|
||||
{disease.treatment.map((step, i) => (
|
||||
<li key={i} className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{step}
|
||||
</li>
|
||||
))}
|
||||
@@ -165,7 +181,7 @@ function DiseaseCard({ disease }: { disease: Disease }) {
|
||||
<span aria-hidden="true">🛡️</span> Prevention Tips
|
||||
</h4>
|
||||
<ul className="space-y-1.5">
|
||||
{disease.preventionTips.map((tip, i) => (
|
||||
{disease.prevention.map((tip, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-start gap-2 text-sm text-zinc-600 dark:text-zinc-300"
|
||||
@@ -182,35 +198,49 @@ function DiseaseCard({ disease }: { disease: Disease }) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Plant Detail Page ─── */
|
||||
// ─── Plant Detail Page ───
|
||||
|
||||
export default async function PlantDetailPage({ params }: Props) {
|
||||
const { plantId } = await params;
|
||||
const plant = getPlantById(plantId);
|
||||
const result = await getPlantWithDiseases(plantId);
|
||||
|
||||
if (!plant) {
|
||||
if (!result) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { plant, diseases } = result;
|
||||
const emoji = getEmojiForCategory(plant.category);
|
||||
const description = getPlantDescription(
|
||||
plant.commonName,
|
||||
plant.scientificName,
|
||||
plant.category,
|
||||
plant.family,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-6 text-sm" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<li>
|
||||
<Link href="/" className="hover:text-leaf-green-600 dark:hover:text-leaf-green-400 transition-colors">
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:text-leaf-green-600 dark:hover:text-leaf-green-400 transition-colors"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li>
|
||||
<Link href="/browse" className="hover:text-leaf-green-600 dark:hover:text-leaf-green-400 transition-colors">
|
||||
<Link
|
||||
href="/browse"
|
||||
className="hover:text-leaf-green-600 dark:hover:text-leaf-green-400 transition-colors"
|
||||
>
|
||||
Browse
|
||||
</Link>
|
||||
</li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li className="text-zinc-800 dark:text-zinc-200 font-medium">
|
||||
{plant.commonName}
|
||||
</li>
|
||||
<li className="text-zinc-800 dark:text-zinc-200 font-medium">{plant.commonName}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -219,7 +249,7 @@ export default async function PlantDetailPage({ params }: Props) {
|
||||
{/* Emoji illustration */}
|
||||
<div className="flex items-center justify-center h-32 w-32 sm:h-40 sm:w-40 shrink-0 rounded-2xl bg-gradient-to-br from-leaf-green-50 to-leaf-green-100 dark:from-leaf-green-950 dark:to-leaf-green-900">
|
||||
<span className="text-6xl sm:text-7xl" role="img" aria-hidden="true">
|
||||
{plant.imageEmoji}
|
||||
{emoji}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -233,11 +263,10 @@ export default async function PlantDetailPage({ params }: Props) {
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Family: <span className="font-medium">{plant.family}</span>
|
||||
{" · "}
|
||||
Category:{" "}
|
||||
<span className="font-medium capitalize">{plant.category}</span>
|
||||
Category: <span className="font-medium capitalize">{plant.category}</span>
|
||||
</p>
|
||||
<p className="mt-3 text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed">
|
||||
{plant.description}
|
||||
{description}
|
||||
</p>
|
||||
<div className="mt-3 flex items-start gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<span aria-hidden="true">💚</span>
|
||||
@@ -258,7 +287,7 @@ export default async function PlantDetailPage({ params }: Props) {
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/browse"
|
||||
href="/upload"
|
||||
className="inline-flex items-center gap-2 shrink-0 rounded-lg bg-leaf-green-600 px-5 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-leaf-green-700 focus:outline-none focus:ring-2 focus:ring-leaf-green-500 focus:ring-offset-2"
|
||||
>
|
||||
📸 Identify a Disease
|
||||
@@ -272,20 +301,22 @@ export default async function PlantDetailPage({ params }: Props) {
|
||||
Known Diseases
|
||||
</h2>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
|
||||
{plant.diseases.length === 0
|
||||
{diseases.length === 0
|
||||
? "No diseases currently documented for this plant."
|
||||
: `${plant.diseases.length} ${plant.diseases.length === 1 ? "disease" : "diseases"} documented for ${plant.commonName}.`}
|
||||
: `${diseases.length} ${diseases.length === 1 ? "disease" : "diseases"} documented for ${plant.commonName}.`}
|
||||
</p>
|
||||
|
||||
{plant.diseases.length > 0 ? (
|
||||
{diseases.length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
{plant.diseases.map((disease) => (
|
||||
{diseases.map((disease) => (
|
||||
<DiseaseCard key={disease.id} disease={disease} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-zinc-300 dark:border-zinc-700 p-10 text-center">
|
||||
<span className="text-4xl block mb-3" aria-hidden="true">🌿</span>
|
||||
<span className="text-4xl block mb-3" aria-hidden="true">
|
||||
🌿
|
||||
</span>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-sm">
|
||||
Disease data for {plant.commonName} is being researched and will be added soon.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user