v0.2.0 — Plant autocomplete + annual/perennial flower categories
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus, Leaf } from "lucide-react";
|
||||
import { CATEGORY_LABELS, CATEGORY_ORDER } from "@/lib/garden/categories";
|
||||
import { searchPlants, type PlantSuggestion } from "@/lib/garden/plant-db";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const schema = z.object({
|
||||
commonName: z.string().min(1, "Name is required"),
|
||||
@@ -34,6 +36,10 @@ type FormValues = z.infer<typeof schema>;
|
||||
|
||||
export function AddPlantButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [suggestions, setSuggestions] = useState<PlantSuggestion[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [category, setCategory] = useState<PlantCategory>("CANOPY_TREE");
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -42,6 +48,23 @@ export function AddPlantButton() {
|
||||
defaultValues: { category: "CANOPY_TREE" },
|
||||
});
|
||||
|
||||
function onNameChange(value: string) {
|
||||
form.setValue("commonName", value);
|
||||
const results = searchPlants(value);
|
||||
setSuggestions(results);
|
||||
setShowSuggestions(results.length > 0);
|
||||
}
|
||||
|
||||
function applySuggestion(suggestion: PlantSuggestion) {
|
||||
form.setValue("commonName", suggestion.commonName);
|
||||
if (suggestion.species) form.setValue("species", suggestion.species);
|
||||
if (suggestion.notes && !form.getValues("notes")) form.setValue("notes", suggestion.notes);
|
||||
form.setValue("category", suggestion.category);
|
||||
setCategory(suggestion.category);
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
const res = await fetch("/api/plants", {
|
||||
method: "POST",
|
||||
@@ -57,13 +80,21 @@ export function AddPlantButton() {
|
||||
toast({ title: "Plant added!", description: `${values.commonName} is now in your food forest.` });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
setCategory("CANOPY_TREE");
|
||||
setSuggestions([]);
|
||||
router.push(`/garden/${plant.id}`);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
setOpen(true);
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)} size="sm">
|
||||
<Button onClick={handleOpen} size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add plant
|
||||
</Button>
|
||||
@@ -76,12 +107,48 @@ export function AddPlantButton() {
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 space-y-1.5">
|
||||
<Label htmlFor="commonName">Common name *</Label>
|
||||
<Input id="commonName" placeholder="Apple, Comfrey, Raspberry…" {...form.register("commonName")} />
|
||||
|
||||
{/* Name with autocomplete */}
|
||||
<div className="col-span-2 space-y-1.5 relative">
|
||||
<Label htmlFor="commonName">What plant is it? *</Label>
|
||||
<Input
|
||||
id="commonName"
|
||||
ref={nameRef}
|
||||
placeholder="Start typing — apple, comfrey, zinnia…"
|
||||
value={form.watch("commonName") ?? ""}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{form.formState.errors.commonName && (
|
||||
<p className="text-xs text-destructive">{form.formState.errors.commonName.message}</p>
|
||||
)}
|
||||
|
||||
{/* Suggestion dropdown */}
|
||||
{showSuggestions && (
|
||||
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-popover border rounded-md shadow-lg overflow-hidden">
|
||||
{suggestions.map((s) => (
|
||||
<button
|
||||
key={`${s.commonName}-${s.category}`}
|
||||
type="button"
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
onMouseDown={() => applySuggestion(s)}
|
||||
>
|
||||
<Leaf className="h-3.5 w-3.5 shrink-0 text-[hsl(var(--leaf))] opacity-70" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">{s.commonName}</p>
|
||||
{s.species && (
|
||||
<p className="text-xs text-muted-foreground italic truncate">{s.species} · {CATEGORY_LABELS[s.category]}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
<div className="px-3 py-2 border-t text-xs text-muted-foreground">
|
||||
Don't see it? Just type the name and fill in the rest yourself.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
@@ -92,8 +159,12 @@ export function AddPlantButton() {
|
||||
<div className="space-y-1.5">
|
||||
<Label>Category *</Label>
|
||||
<Select
|
||||
defaultValue="CANOPY_TREE"
|
||||
onValueChange={(v) => form.setValue("category", v as PlantCategory)}
|
||||
value={category}
|
||||
onValueChange={(v) => {
|
||||
const c = v as PlantCategory;
|
||||
form.setValue("category", c);
|
||||
setCategory(c);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -107,7 +178,7 @@ export function AddPlantButton() {
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 space-y-1.5">
|
||||
<Label htmlFor="species">Scientific name (optional)</Label>
|
||||
<Label htmlFor="species">Scientific name <span className="text-muted-foreground font-normal">(filled in automatically if known)</span></Label>
|
||||
<Input id="species" placeholder="Malus domestica" className="italic" {...form.register("species")} />
|
||||
</div>
|
||||
|
||||
@@ -128,7 +199,7 @@ export function AddPlantButton() {
|
||||
|
||||
<div className="col-span-2 space-y-1.5">
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Textarea id="notes" placeholder="Any notes about this plant…" rows={3} {...form.register("notes")} />
|
||||
<Textarea id="notes" placeholder="Anything worth remembering about this plant…" rows={3} {...form.register("notes")} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user