diff --git a/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx b/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx index 2304364..e4e0d35 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx @@ -1,6 +1,11 @@ -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { Sprout, Calendar } from "lucide-react"; -import { Crop } from "@/types"; +"use client"; + +import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card"; +import { Sprout, Calendar, ArrowRight, BarChart } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import type { Crop } from "@/types"; interface CropCardProps { crop: Crop; @@ -9,32 +14,85 @@ interface CropCardProps { export function CropCard({ crop, onClick }: CropCardProps) { const statusColors = { - growing: "text-green-500", - harvested: "text-yellow-500", - planned: "text-blue-500", + growing: { + bg: "bg-green-50 dark:bg-green-900", + text: "text-green-600 dark:text-green-300", + border: "border-green-200", + }, + harvested: { + bg: "bg-yellow-50 dark:bg-yellow-900", + text: "text-yellow-600 dark:text-yellow-300", + border: "border-yellow-200", + }, + planned: { + bg: "bg-blue-50 dark:bg-blue-900", + text: "text-blue-600 dark:text-blue-300", + border: "border-blue-200", + }, }; + const statusColor = statusColors[crop.status as keyof typeof statusColors]; + return ( + className={`w-full h-full overflow-hidden transition-all duration-200 hover:shadow-lg border-muted/60 bg-white dark:bg-slate-800 hover:bg-muted/10 dark:hover:bg-slate-700`}>
-
- + + {crop.status} + +
+ + {crop.plantedDate.toLocaleDateString()}
- {crop.status}
-
-

{crop.name}

-
- -

Planted: {crop.plantedDate.toLocaleDateString()}

+
+
+ +
+
+

{crop.name}

+

+ {crop.variety} • {crop.area} +

+ + {crop.status !== "planned" && ( +
+
+ Progress + {crop.progress}% +
+ +
+ )} + + {crop.status === "growing" && ( +
+
+ + Health: {crop.healthScore}% +
+
+ )}
+ + + ); } diff --git a/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx b/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx index 5173479..c57f3cd 100644 --- a/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx +++ b/frontend/app/(sidebar)/farms/[farmId]/crop-dialog.tsx @@ -69,15 +69,15 @@ export function CropDialog({ open, onOpenChange, onSubmit }: CropDialogProps) {
{/* Left side - Plant Selection */} -
+

Select Plant to Grow

{plants.map((plant) => ( setSelectedPlant(plant.id)}>
@@ -101,25 +101,15 @@ export function CropDialog({ open, onOpenChange, onSubmit }: CropDialogProps) { {/* Right side - Map */}
-
-
+
+
- {/*
- -

- Map placeholder -
- Lat: {location.lat.toFixed(4)} -
- Lng: {location.lng.toFixed(4)} -

-
*/}
{/* Footer */} -
+
+
+
+
+ {/* Breadcrumbs */} + -
- - -
-
- -
-

{farm?.name ?? "Unknown Farm"}

-
-
- - {farm?.location ?? "Unknown Location"} -
-
- -
-
- Farm Type: - {farm?.type ?? "Unknown Type"} -
-
- Created: - {farm?.createdAt?.toLocaleDateString() ?? "Unknown Date"} -
-
- Total Crops: - {crops.length} -
-
-
-
+ {/* Back button */} + -
-

Crops

- -
- {/* Clickable "Add Crop" Card */} - setIsDialogOpen(true)}> - -
-
- -
-
-

Add Crop

-

Plant a new crop

+ {/* Error state */} + {error && ( + + + Error + {error} + + )} + + {/* Loading state */} + {isLoading && ( +
+ +

Loading farm details...

+
+ )} + + {/* Farm details */} + {!isLoading && !error && farm && ( + <> +
+ {/* Farm info card */} + + +
+ + {farm.type} + +
+ + Created {farm.createdAt.toLocaleDateString()} +
+
+
+
+ +
+
+

{farm.name}

+
+ + {farm.location} +
+
+
+
+ +
+
+

Total Area

+

{farm.area}

+
+
+

Total Crops

+

{farm.crops}

+
+
+

Growing Crops

+

{cropCounts.growing}

+
+
+

Harvested

+

{cropCounts.harvested}

+
+
+
+
+ + {/* Weather card */} + + + Current Conditions + Weather at your farm location + + +
+
+
+ +
+
+

Temperature

+

{farm.weather?.temperature}°C

+
+
+
+
+ +
+
+

Humidity

+

{farm.weather?.humidity}%

+
+
+
+
+ +
+
+

Sunlight

+

{farm.weather?.sunlight}%

+
+
+
+
+ +
+
+

Rainfall

+

{farm.weather?.rainfall}

+
+
+
+
+
+
+ + {/* Crops section */} +
+
+
+

+ + Crops +

+

Manage and monitor all crops in this farm

+
- - - {/* New Crop Dialog */} - + + + setActiveFilter("all")}> + All Crops ({cropCounts.all}) + + setActiveFilter("growing")}> + Growing ({cropCounts.growing}) + + setActiveFilter("planned")}> + Planned ({cropCounts.planned}) + + setActiveFilter("harvested")}> + Harvested ({cropCounts.harvested}) + + - {crops.map((crop) => ( - { - router.push(`/farms/${crop.farmId}/crops/${crop.id}`); - }} - /> - ))} -
+ + {filteredCrops.length === 0 ? ( +
+
+ +
+

No crops found

+

+ {activeFilter === "all" + ? "You haven't added any crops to this farm yet." + : `No ${activeFilter} crops found. Try a different filter.`} +

+ +
+ ) : ( +
+ + {filteredCrops.map((crop, index) => ( + + router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} + /> + + ))} + +
+ )} +
+ + {/* Growing tab */} + + {filteredCrops.length === 0 ? ( +
+
+ +
+

No growing crops

+

+ You don't have any growing crops in this farm yet. +

+ +
+ ) : ( +
+ + {filteredCrops.map((crop, index) => ( + + router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} + /> + + ))} + +
+ )} +
+ + {/* Planned tab */} + + {filteredCrops.length === 0 ? ( +
+

No planned crops

+

+ You don't have any planned crops in this farm yet. +

+ +
+ ) : ( +
+ + {filteredCrops.map((crop, index) => ( + + router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} + /> + + ))} + +
+ )} +
+ + {/* Harvested tab */} + + {filteredCrops.length === 0 ? ( +
+

No harvested crops

+

+ You don't have any harvested crops in this farm yet. +

+ +
+ ) : ( +
+ + {filteredCrops.map((crop, index) => ( + + router.push(`/farms/${crop.farmId}/crops/${crop.id}`)} + /> + + ))} + +
+ )} +
+ +
+ + )}
+ + {/* Add Crop Dialog */} +
); } diff --git a/frontend/app/(sidebar)/farms/add-farm-form.tsx b/frontend/app/(sidebar)/farms/add-farm-form.tsx index 288197a..840bece 100644 --- a/frontend/app/(sidebar)/farms/add-farm-form.tsx +++ b/frontend/app/(sidebar)/farms/add-farm-form.tsx @@ -7,8 +7,16 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { useState } from "react"; +import { Loader2 } from "lucide-react"; import type { Farm } from "@/types"; -import { farmFormSchema } from "@/schemas/form.schema"; + +const farmFormSchema = z.object({ + name: z.string().min(2, "Farm name must be at least 2 characters"), + location: z.string().min(2, "Location must be at least 2 characters"), + type: z.string().min(1, "Please select a farm type"), + area: z.string().optional(), +}); interface AddFarmFormProps { onSubmit: (data: Partial) => Promise; @@ -16,18 +24,33 @@ interface AddFarmFormProps { } export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const form = useForm>({ resolver: zodResolver(farmFormSchema), defaultValues: { name: "", location: "", type: "", + area: "", }, }); + const handleSubmit = async (values: z.infer) => { + try { + setIsSubmitting(true); + await onSubmit(values); + form.reset(); + } catch (error) { + console.error("Error submitting form:", error); + } finally { + setIsSubmitting(false); + } + }; + return (
- + - This is your farm's display name. + This is your farm's display name. )} @@ -52,6 +75,7 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) { + City, region or specific address )} @@ -73,6 +97,7 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) { Durian Mango Rice + Mixed Crops Other @@ -81,11 +106,35 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) { )} /> + ( + + Total Area (optional) + + + + The total size of your farm + + + )} + /> +
- - +
diff --git a/frontend/app/(sidebar)/farms/farm-card.tsx b/frontend/app/(sidebar)/farms/farm-card.tsx index 210cd1c..50f99c2 100644 --- a/frontend/app/(sidebar)/farms/farm-card.tsx +++ b/frontend/app/(sidebar)/farms/farm-card.tsx @@ -1,5 +1,10 @@ -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { MapPin, Sprout, Plus } from "lucide-react"; +"use client"; + +import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card"; +import { MapPin, Sprout, Plus, CalendarDays, ArrowRight } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; import type { Farm } from "@/types"; export interface FarmCardProps { @@ -9,50 +14,81 @@ export interface FarmCardProps { } export function FarmCard({ variant, farm, onClick }: FarmCardProps) { - const cardClasses = - "w-full max-w-[240px] bg-muted/50 hover:bg-muted/80 transition-all cursor-pointer group hover:shadow-lg"; + const cardClasses = cn( + "w-full h-full overflow-hidden transition-all duration-200 hover:shadow-lg border", + variant === "add" + ? "bg-green-50/50 dark:bg-green-900/50 hover:bg-green-50/80 dark:hover:bg-green-900/80 border-dashed border-muted/60" + : "bg-white dark:bg-slate-800 hover:bg-muted/10 dark:hover:bg-slate-700 border-muted/60" + ); if (variant === "add") { return ( - -
-
- -
-
-

Setup

-

Setup new farm

-
+
+
+
- +

Add New Farm

+

Create a new farm to manage your crops and resources

+
); } if (variant === "farm" && farm) { + const formattedDate = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }).format(farm.createdAt); + return (
-
- + + {farm.type} + +
+ + {formattedDate}
- {farm.type}
-
+
+
+ +
-

{farm.name}

-
- -

{farm.location}

+

{farm.name}

+
+ + {farm.location} +
+
+
+

Area

+

{farm.area}

+
+
+

Crops

+

{farm.crops}

+
-
Created {farm.createdAt.toLocaleDateString()}
+ + + ); } diff --git a/frontend/app/(sidebar)/farms/page.tsx b/frontend/app/(sidebar)/farms/page.tsx index e123112..af9b4b9 100644 --- a/frontend/app/(sidebar)/farms/page.tsx +++ b/frontend/app/(sidebar)/farms/page.tsx @@ -1,87 +1,306 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { Search, Plus, Filter, SlidersHorizontal, Leaf, Calendar, AlertTriangle, Loader2 } from "lucide-react"; + import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; -import { Link, Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuLabel, +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; + import { FarmCard } from "./farm-card"; import { AddFarmForm } from "./add-farm-form"; -import { useRouter } from "next/navigation"; import type { Farm } from "@/types"; +import { fetchFarms } from "@/api/farm"; +/** + * FarmSetupPage component allows users to search, filter, sort, and add farms. + */ export default function FarmSetupPage() { const router = useRouter(); + + // Component state const [isDialogOpen, setIsDialogOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const [farms, setFarms] = useState([ - { - id: "1", - name: "Green Valley Farm", - location: "Bangkok", - type: "Durian", - createdAt: new Date(), - }, - ]); + const [farms, setFarms] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [activeFilter, setActiveFilter] = useState("all"); + const [sortOrder, setSortOrder] = useState<"newest" | "oldest" | "alphabetical">("newest"); + // Load farms when the component mounts. + useEffect(() => { + async function loadFarms() { + try { + setIsLoading(true); + setError(null); + const data = await fetchFarms(); + setFarms(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An unknown error occurred"); + } finally { + setIsLoading(false); + } + } + + loadFarms(); + }, []); + + /** + * Handles adding a new farm. + * + * @param data - Partial Farm data from the form. + */ const handleAddFarm = async (data: Partial) => { - const newFarm: Farm = { - id: Math.random().toString(36).substr(2, 9), - name: data.name!, - location: data.location!, - type: data.type!, - createdAt: new Date(), - }; - setFarms([...farms, newFarm]); - setIsDialogOpen(false); + try { + // Simulate an API call delay for adding a new farm. + await new Promise((resolve) => setTimeout(resolve, 800)); + + const newFarm: Farm = { + id: Math.random().toString(36).substr(2, 9), + name: data.name!, + location: data.location!, + type: data.type!, + createdAt: new Date(), + area: data.area || "0 hectares", + crops: 0, + }; + + setFarms((prev) => [newFarm, ...prev]); + setIsDialogOpen(false); + } catch (err) { + setError("Failed to add farm. Please try again."); + } }; - const filteredFarms = farms.filter( - (farm) => - farm.name.toLowerCase().includes(searchQuery.toLowerCase()) || - farm.location.toLowerCase().includes(searchQuery.toLowerCase()) || - farm.type.toLowerCase().includes(searchQuery.toLowerCase()) - ); + // Filter and sort farms based on the current state. + const filteredAndSortedFarms = farms + .filter( + (farm) => + (activeFilter === "all" || farm.type === activeFilter) && + (farm.name.toLowerCase().includes(searchQuery.toLowerCase()) || + farm.location.toLowerCase().includes(searchQuery.toLowerCase()) || + farm.type.toLowerCase().includes(searchQuery.toLowerCase())) + ) + .sort((a, b) => { + if (sortOrder === "newest") { + return b.createdAt.getTime() - a.createdAt.getTime(); + } else if (sortOrder === "oldest") { + return a.createdAt.getTime() - b.createdAt.getTime(); + } else { + return a.name.localeCompare(b.name); + } + }); + + // Get available farm types for filters. + const farmTypes = ["all", ...new Set(farms.map((farm) => farm.type))]; return ( -
-
-

Farms

-
- - setSearchQuery(e.target.value)} - /> +
+
+
+ {/* Header */} +
+
+

Your Farms

+

Manage and monitor all your agricultural properties

+
+
+
+ + setSearchQuery(e.target.value)} + /> +
+ +
+
+ + {/* Filtering and sorting controls */} +
+
+ {farmTypes.map((type) => ( + setActiveFilter(type)}> + {type === "all" ? "All Farms" : type} + + ))} +
+ + + + + + Sort by + + setSortOrder("newest")}> + + Newest first + {sortOrder === "newest" && } + + setSortOrder("oldest")}> + + Oldest first + {sortOrder === "oldest" && } + + setSortOrder("alphabetical")}> + + Alphabetical + {sortOrder === "alphabetical" && } + + + +
+ + + + {/* Error state */} + {error && ( + + + Error + {error} + + )} + + {/* Loading state */} + {isLoading && ( +
+ +

Loading your farms...

+
+ )} + + {/* Empty state */} + {!isLoading && !error && filteredAndSortedFarms.length === 0 && ( +
+
+ +
+

No farms found

+ {searchQuery || activeFilter !== "all" ? ( +

+ No farms match your current filters. Try adjusting your search or filters. +

+ ) : ( +

+ You haven't added any farms yet. Get started by adding your first farm. +

+ )} + +
+ )} + + {/* Grid of farm cards */} + {!isLoading && !error && filteredAndSortedFarms.length > 0 && ( +
+ + + setIsDialogOpen(true)} /> + + + {filteredAndSortedFarms.map((farm, index) => ( + + router.push(`/farms/${farm.id}`)} /> + + ))} + +
+ )}
- -
- - setIsDialogOpen(true)} /> - - - Setup New Farm - Fill out the form to configure your new farm. - - setIsDialogOpen(false)} /> - - - - {filteredFarms.map((farm) => ( - { - router.push(`/farms/${farm.id}`); - }} - /> - ))} -
+ {/* Add Farm Dialog */} + + + + Add New Farm + Fill out the details below to add a new farm to your account. + + setIsDialogOpen(false)} /> + +
); } + +/** + * A helper component to render the Check icon. + * + * @param props - Optional className for custom styling. + */ +function Check({ className }: { className?: string }) { + return ( + + + + ); +}