flag impl fin

This commit is contained in:
2026-06-06 17:22:31 -04:00
parent db4c656730
commit 96de91e86c
12 changed files with 1025 additions and 65 deletions

View File

@@ -10,6 +10,7 @@ import { v4 as uuidv4 } from "uuid";
const VALID_CONTENT_TYPES = [
"plant_image",
"disease_image",
"disease_description",
"disease_symptoms",
"disease_causes",
"disease_treatment",

View File

@@ -140,9 +140,19 @@ function DiseaseCard({
/>
</div>
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed mb-4">
{disease.description}
</p>
<div className="flex items-start justify-between gap-4 mb-4">
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed">
{disease.description}
</p>
<FlagButton
contentType="disease_description"
contentId={disease.id}
fieldName="description"
label="description"
small
className="shrink-0 mt-0.5"
/>
</div>
{/* Details grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -4,6 +4,7 @@ import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { getPlantWithDiseases } from "@/lib/api/diseases-db";
import { getPlantDescription } from "@/lib/display-helpers";
import BetaNotice from "@/components/BetaNotice";
import DiseaseCards from "./DiseaseCards";
import PlantViewTracker from "@/components/PlantViewTracker";
import FlagPlantImage from "@/components/FlagPlantImage";
@@ -83,6 +84,8 @@ export default async function PlantDetailPage({ params }: Props) {
</ol>
</nav>
<BetaNotice variant="card" className="mb-6" />
{/* Plant hero */}
<div className="flex flex-col sm:flex-row sm:items-start gap-6 mb-10">
{/* Plant image */}

View File

@@ -1,7 +1,8 @@
import React, { Suspense } from "react";
import { Suspense } from "react";
import { getBrowsePlants } from "@/lib/api/browse";
import BrowseContent from "./BrowseContent";
import { PlantCardSkeleton } from "@/components/LoadingSkeleton";
import BetaNotice from "@/components/BetaNotice";
/**
* Browse page — fetches plants with disease counts from the database
@@ -12,27 +13,30 @@ export default async function BrowsePage() {
const allPlants = await getBrowsePlants();
return (
<Suspense
fallback={
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
<div className="mb-8">
<div className="h-9 w-48 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
<div className="mt-2 h-5 w-72 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
<>
<BetaNotice variant="full-width" />
<Suspense
fallback={
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
<div className="mb-8">
<div className="h-9 w-48 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
<div className="mt-2 h-5 w-72 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
</div>
<div className="mb-6 h-12 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-zinc-700" />
<div className="flex gap-2 mb-8">
{Array.from({ length: 5 }, (_, i) => (
<div
key={i}
className="h-9 w-24 animate-pulse rounded-full bg-zinc-200 dark:bg-zinc-700"
/>
))}
</div>
<PlantCardSkeleton count={8} />
</div>
<div className="mb-6 h-12 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-zinc-700" />
<div className="flex gap-2 mb-8">
{Array.from({ length: 5 }, (_, i) => (
<div
key={i}
className="h-9 w-24 animate-pulse rounded-full bg-zinc-200 dark:bg-zinc-700"
/>
))}
</div>
<PlantCardSkeleton count={8} />
</div>
}
>
<BrowseContent allPlants={allPlants} />
</Suspense>
}
>
<BrowseContent allPlants={allPlants} />
</Suspense>
</>
);
}

View File

@@ -0,0 +1,44 @@
/**
* BetaNotice — a banner informing users that the site is in beta,
* community-driven, and most data isn't reviewed by humans yet.
* Encourages use of the Flag button to flag content for review.
*
* Two layout variants:
* - "full-width" (default): stretches edge-to-edge with an inner max-w wrapper
* - "card": rounded card with border, suitable for inside content containers
*/
export default function BetaNotice({
variant = "full-width",
className = "",
}: {
variant?: "full-width" | "card";
className?: string;
}) {
const containerClasses =
variant === "card"
? `rounded-xl bg-warning-amber-50 dark:bg-warning-amber-950/60 border border-warning-amber-200 dark:border-warning-amber-800 ${className}`
: `bg-warning-amber-50 dark:bg-warning-amber-950/60 border-b border-warning-amber-200 dark:border-warning-amber-800 ${className}`;
return (
<div className={containerClasses}>
<div
className={
variant === "card" ? "px-4 sm:px-6 py-3" : "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-3"
}
>
<p className="text-xs sm:text-sm text-warning-amber-800 dark:text-warning-amber-200 text-center leading-relaxed">
<span className="font-semibold">🚧 Beta Community Driven.</span> Most data here is not
reviewed by humans. Spot something wrong or it could be better? Use the{" "}
<span className="inline-flex items-center gap-1 font-medium whitespace-nowrap">
<svg className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M3.5 2.75a.75.75 0 00-1.5 0v14.5a.75.75 0 001.5 0v-4.392l1.657-.348a6.453 6.453 0 014.271.572 7.948 7.948 0 005.965.524l2.078-.64A.75.75 0 0018 12.25v-8.5a.75.75 0 00-.904-.734l-2.38.501a7.25 7.25 0 01-4.186-.363l-.502-.2a8.75 8.75 0 00-5.053-.439l-1.475.31V2.75z" />
</svg>
Flag
</span>{" "}
button on any image or description to flag it for review.
</p>
</div>
</div>
);
}

View File

@@ -132,9 +132,18 @@ export default function DiseaseCard({
{/* Full description */}
<div>
<h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 mb-1">
Description
</h4>
<div className="flex items-center justify-between mb-1">
<h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Description
</h4>
<FlagButton
contentType="disease_description"
contentId={disease.id}
fieldName="description"
label="description"
small
/>
</div>
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{disease.description}
</p>

View File

@@ -8,6 +8,7 @@ import { useState, useCallback } from "react";
export type FlagContentType =
| "plant_image"
| "disease_image"
| "disease_description"
| "disease_symptoms"
| "disease_causes"
| "disease_treatment"

View File

@@ -1,3 +1,5 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import FlagButton from "@/components/FlagButton";

View File

@@ -130,6 +130,7 @@ export const flaggedContent = sqliteTable(
enum: [
"plant_image",
"disease_image",
"disease_description",
"disease_symptoms",
"disease_causes",
"disease_treatment",