prevelance data added
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user