prevelance data added

This commit is contained in:
2026-06-07 12:06:41 -04:00
parent cc7b2a593a
commit 876c26968b
11 changed files with 2305 additions and 148 deletions

View File

@@ -272,18 +272,22 @@ function PrevalenceBadge({ prevalence }: { prevalence: Prevalence }) {
common: "📊",
uncommon: "📋",
rare: "📌",
very_rare: "🔍",
};
const colors: Record<Prevalence, string> = {
common: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300",
uncommon: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-300",
rare: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300",
very_rare: "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300",
};
const label = prevalence.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
return (
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[prevalence]}`}
>
{icons[prevalence]} {prevalence.charAt(0).toUpperCase() + prevalence.slice(1)}
{icons[prevalence]} {label}
</span>
);
}
@@ -298,9 +302,10 @@ const SEVERITY_RANK: Record<Severity, number> = {
};
const PREVALENCE_RANK: Record<Prevalence, number> = {
common: 3,
uncommon: 2,
rare: 1,
common: 4,
uncommon: 3,
rare: 2,
very_rare: 1,
};
type SortField = "prevalence" | "danger";

View File

@@ -3,7 +3,7 @@
* for the browse page. Runs server-side only.
*/
import { sql, eq } from "drizzle-orm";
import { sql, eq, inArray, notInArray } from "drizzle-orm";
import { getDb } from "@/lib/db/index";
import { plants, diseases, plantViews } from "@/lib/db/schema";
import type { PlantCardData } from "@/components/PlantCard";
@@ -12,11 +12,13 @@ export type { PlantCardData };
/**
* Get all plants with their disease counts for the browse page.
*
* Uses scalar subqueries for COUNT to avoid expensive LEFT JOIN + GROUP BY
* on the large diseases table (11,498 rows).
*/
export async function getBrowsePlants(): Promise<PlantCardData[]> {
const db = getDb();
// LEFT JOIN to include plants with zero diseases
const rows = await db
.select({
id: plants.id,
@@ -27,12 +29,10 @@ export async function getBrowsePlants(): Promise<PlantCardData[]> {
imageUrl: plants.imageUrl,
updatedAt: plants.updatedAt,
viewCount: sql<number>`COALESCE(${plantViews.viewCount}, 0)`,
diseaseCount: sql<number>`COUNT(${diseases.id})`,
diseaseCount: sql<number>`(SELECT COUNT(*) FROM ${diseases} WHERE ${diseases.plantId} = ${plants.id})`,
})
.from(plants)
.leftJoin(diseases, eq(diseases.plantId, plants.id))
.leftJoin(plantViews, eq(plantViews.plantId, plants.id))
.groupBy(plants.id)
.orderBy(plants.commonName);
return rows.map((r) => ({
@@ -61,12 +61,10 @@ export async function getBrowsePlant(id: string): Promise<PlantCardData | null>
family: plants.family,
category: plants.category,
imageUrl: plants.imageUrl,
diseaseCount: sql<number>`COUNT(${diseases.id})`,
diseaseCount: sql<number>`(SELECT COUNT(*) FROM ${diseases} WHERE ${diseases.plantId} = ${plants.id})`,
})
.from(plants)
.leftJoin(diseases, eq(diseases.plantId, plants.id))
.where(eq(plants.id, id))
.groupBy(plants.id)
.limit(1);
return rows[0] ?? null;
@@ -91,12 +89,47 @@ const FEATURED_IDS = [
];
export async function getFeaturedPlants(): Promise<PlantCardData[]> {
const all = await getBrowsePlants();
const featured = all.filter((p) => FEATURED_IDS.includes(p.id));
// If fewer than expected are found, pad with first available plants
if (featured.length < 6) {
const rest = all.filter((p) => !FEATURED_IDS.includes(p.id));
return [...featured, ...rest].slice(0, 12);
const db = getDb();
const selectFeatured = db
.select({
id: plants.id,
commonName: plants.commonName,
scientificName: plants.scientificName,
family: plants.family,
category: plants.category,
imageUrl: plants.imageUrl,
updatedAt: plants.updatedAt,
viewCount: sql<number>`COALESCE(${plantViews.viewCount}, 0)`,
diseaseCount: sql<number>`(SELECT COUNT(*) FROM ${diseases} WHERE ${diseases.plantId} = ${plants.id})`,
})
.from(plants)
.leftJoin(plantViews, eq(plantViews.plantId, plants.id));
const rows = await selectFeatured
.where(inArray(plants.id, FEATURED_IDS))
.orderBy(plants.commonName);
if (rows.length < 6) {
const padRows = await selectFeatured
.where(notInArray(plants.id, FEATURED_IDS))
.orderBy(plants.commonName)
.limit(12 - rows.length);
return [...rows, ...padRows].map(mapRow);
}
return featured.slice(0, 12);
return rows.slice(0, 12).map(mapRow);
}
function mapRow(r: Record<string, unknown>): PlantCardData {
return {
id: r.id as string,
commonName: r.commonName as string,
scientificName: r.scientificName as string,
family: r.family as string,
category: r.category as string,
imageUrl: r.imageUrl as string,
updatedAt: r.updatedAt as string | undefined,
viewCount: r.viewCount as number,
diseaseCount: r.diseaseCount as number,
};
}

View File

@@ -280,7 +280,7 @@ export async function validateKnowledgeBase(): Promise<string[]> {
"environmental",
];
const validSeverities: Severity[] = ["low", "moderate", "high", "critical"];
const validPrevalences: Prevalence[] = ["common", "uncommon", "rare"];
const validPrevalences: Prevalence[] = ["common", "uncommon", "rare", "very_rare"];
const db = getDb();

View File

@@ -55,10 +55,11 @@ export const diseases = sqliteTable(
prevention: text("prevention", { mode: "json" }).notNull().default([]).$type<string[]>(),
lookalikeIds: text("lookalike_ids", { mode: "json" }).notNull().default([]).$type<string[]>(),
prevalence: text("prevalence", {
enum: ["common", "uncommon", "rare"],
enum: ["common", "uncommon", "rare", "very_rare"],
})
.notNull()
.default("uncommon"),
prevalenceScore: integer("prevalence_score").notNull().default(0),
severity: text("severity", {
enum: ["low", "moderate", "high", "critical"],
}).notNull(),

View File

@@ -10,7 +10,7 @@ export type CausalAgentType = "fungal" | "bacterial" | "viral" | "environmental"
export type Severity = "low" | "moderate" | "high" | "critical";
/** How common/prevalent a disease is in the field */
export type Prevalence = "common" | "uncommon" | "rare";
export type Prevalence = "common" | "uncommon" | "rare" | "very_rare";
/** Plant category for grouping and filtering */
export type PlantCategory =