feat: add farm setup and fetch

This commit is contained in:
Sosokker 2025-03-28 01:20:41 +07:00
parent 21559f3b55
commit 6885eb4bfb
8 changed files with 4228 additions and 3571 deletions

View File

@ -1,198 +1,100 @@
import axiosInstance from "./config";
import type { Crop, CropAnalytics, Farm } from "@/types";
import type { Farm } from "@/types";
/**
* Fetch a specific crop by id using axios.
* Falls back to dummy data on error.
*/
export async function fetchCropById(id: string): Promise<Crop> {
try {
const response = await axiosInstance.get<Crop>(`/api/crops/${id}`);
return response.data;
} catch (error) {
// Fallback dummy data
return {
id,
farmId: "1",
name: "Monthong Durian",
plantedDate: new Date("2024-01-15"),
status: "growing",
variety: "Premium Grade",
expectedHarvest: new Date("2024-07-15"),
area: "2.5 hectares",
healthScore: 85,
};
}
}
/**
* Fetch crop analytics by crop id using axios.
* Returns dummy analytics if the API call fails.
*/
export async function fetchAnalyticsByCropId(id: string): Promise<CropAnalytics> {
try {
const response = await axiosInstance.get<CropAnalytics>(`/api/crops/${id}/analytics`);
return response.data;
} catch (error) {
return {
cropId: id,
growthProgress: 45,
humidity: 75,
temperature: 28,
sunlight: 85,
waterLevel: 65,
plantHealth: "good",
nextAction: "Water the plant",
nextActionDue: new Date("2024-02-15"),
soilMoisture: 70,
windSpeed: "12 km/h",
rainfall: "25mm last week",
nutrientLevels: {
nitrogen: 80,
phosphorus: 65,
potassium: 75,
},
};
}
}
/**
* Fetch an array of farms using axios.
* Simulates a delay and a random error; returns dummy data if the API is unavailable.
* Fetch an array of farms.
* Calls GET /farms and returns fallback dummy data on failure.
*/
export async function fetchFarms(): Promise<Farm[]> {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
const response = await axiosInstance.get<Farm[]>("/api/farms");
return response.data;
} catch (error) {
// Optionally, you could simulate a random error here. For now we return fallback data.
return [
{
id: "1",
name: "Green Valley Farm",
location: "Bangkok",
type: "durian",
createdAt: new Date("2023-01-01"),
area: "12.5 hectares",
crops: 5,
},
{
id: "2",
name: "Sunrise Orchard",
location: "Chiang Mai",
type: "mango",
createdAt: new Date("2023-02-15"),
area: "8.3 hectares",
crops: 3,
},
{
id: "3",
name: "Golden Harvest Fields",
location: "Phuket",
type: "rice",
createdAt: new Date("2023-03-22"),
area: "20.1 hectares",
crops: 2,
},
];
}
return axiosInstance.get<Farm[]>("/farms").then((res) => res.data);
}
/**
* Simulates creating a new farm.
* Waits for 800ms and then uses dummy data.
* Create a new farm.
* Calls POST /farms with a payload that uses snake_case keys.
*/
export async function createFarm(data: Partial<Farm>): Promise<Farm> {
await new Promise((resolve) => setTimeout(resolve, 800));
// In a real implementation you might call:
// const response = await axiosInstance.post<Farm>("/api/farms", data);
// return response.data;
return {
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,
};
return axiosInstance.post<Farm>("/farms", data).then((res) => res.data);
}
// Additional functions for fetching crop details remain unchanged...
/**
* Fetch detailed information for a specific farm (including its crops) using axios.
* If the API call fails, returns fallback dummy data.
* Fetch a specific farm by ID.
* Calls GET /farms/{farm_id} and returns fallback data on failure.
*/
export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; crops: Crop[] }> {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 1200));
export async function getFarm(farmId: string): Promise<Farm> {
// Simulate a network delay.
await new Promise((resolve) => setTimeout(resolve, 600));
try {
const response = await axiosInstance.get<{ farm: Farm; crops: Crop[] }>(`/api/farms/${farmId}`);
const response = await axiosInstance.get<Farm>(`/farms/${farmId}`);
return response.data;
} catch (error) {
// If the given farmId is "999", simulate a not found error.
if (farmId === "999") {
throw new Error("FARM_NOT_FOUND");
}
const farm: Farm = {
id: farmId,
name: "Green Valley Farm",
location: "Bangkok, Thailand",
type: "durian",
createdAt: new Date("2023-01-15"),
area: "12.5 hectares",
crops: 3,
// Additional details such as weather can be included if needed.
weather: {
temperature: 28,
humidity: 75,
rainfall: "25mm last week",
sunlight: 85,
},
} catch (error: any) {
console.error(`Error fetching farm ${farmId}. Returning fallback data:`, error);
const dummyDate = new Date().toISOString();
return {
CreatedAt: dummyDate,
FarmType: "conventional",
Lat: 15.87,
Lon: 100.9925,
Name: "Fallback Farm",
OwnerID: "fallback_owner",
TotalSize: "40 hectares",
UUID: farmId,
UpdatedAt: dummyDate,
};
const crops: Crop[] = [
{
id: "1",
farmId,
name: "Monthong Durian",
plantedDate: new Date("2023-03-15"),
status: "growing",
variety: "Premium",
area: "4.2 hectares",
healthScore: 92,
progress: 65,
},
{
id: "2",
farmId,
name: "Chanee Durian",
plantedDate: new Date("2023-02-20"),
status: "planned",
variety: "Standard",
area: "3.8 hectares",
healthScore: 0,
progress: 0,
},
{
id: "3",
farmId,
name: "Kradum Durian",
plantedDate: new Date("2022-11-05"),
status: "harvested",
variety: "Premium",
area: "4.5 hectares",
healthScore: 100,
progress: 100,
},
];
return { farm, crops };
}
}
/**
* Update an existing farm.
* Calls PUT /farms/{farm_id} with a snake_case payload.
*/
export async function updateFarm(
farmId: string,
data: {
farm_type: string;
lat: number;
lon: number;
name: string;
total_size: string;
}
): Promise<Farm> {
// Simulate a network delay.
await new Promise((resolve) => setTimeout(resolve, 800));
try {
const response = await axiosInstance.put<Farm>(`/farms/${farmId}`, data);
return response.data;
} catch (error: any) {
console.error(`Error updating farm ${farmId}. Returning fallback data:`, error);
const now = new Date().toISOString();
return {
CreatedAt: now,
FarmType: data.farm_type,
Lat: data.lat,
Lon: data.lon,
Name: data.name,
OwnerID: "updated_owner",
TotalSize: data.total_size,
UUID: farmId,
UpdatedAt: now,
};
}
}
/**
* Delete a specific farm.
* Calls DELETE /farms/{farm_id} and returns a success message.
*/
export async function deleteFarm(farmId: string): Promise<{ message: string }> {
// Simulate a network delay.
await new Promise((resolve) => setTimeout(resolve, 500));
try {
await axiosInstance.delete(`/farms/${farmId}`);
return { message: "Farm deleted successfully" };
} catch (error: any) {
console.error(`Error deleting farm ${farmId}. Assuming deletion was successful:`, error);
return { message: "Farm deleted successfully (dummy)" };
}
}

View File

@ -1,459 +1,463 @@
"use client";
// "use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import {
ArrowLeft,
MapPin,
Plus,
Sprout,
Calendar,
LayoutGrid,
AlertTriangle,
Loader2,
Home,
ChevronRight,
Droplets,
Sun,
Wind,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { CropDialog } from "./crop-dialog";
import { CropCard } from "./crop-card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { motion, AnimatePresence } from "framer-motion";
import type { Farm, Crop } from "@/types";
import { fetchFarmDetails } from "@/api/farm";
// import React, { useState, useEffect } from "react";
// import { useRouter } from "next/navigation";
// import {
// ArrowLeft,
// MapPin,
// Plus,
// Sprout,
// Calendar,
// LayoutGrid,
// AlertTriangle,
// Loader2,
// Home,
// ChevronRight,
// Droplets,
// Sun,
// Wind,
// } from "lucide-react";
// import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
// import { Button } from "@/components/ui/button";
// import { CropDialog } from "./crop-dialog";
// import { CropCard } from "./crop-card";
// import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
// import { Badge } from "@/components/ui/badge";
// import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// import { motion, AnimatePresence } from "framer-motion";
// import type { Farm, Crop } from "@/types";
// import { fetchFarmDetails } from "@/api/farm";
/**
* Used in Next.js; params is now a Promise and must be unwrapped with React.use()
*/
interface FarmDetailPageProps {
params: Promise<{ farmId: string }>;
}
// /**
// * Used in Next.js; params is now a Promise and must be unwrapped with React.use()
// */
// interface FarmDetailPageProps {
// params: Promise<{ farmId: string }>;
// }
// export default function FarmDetailPage({ params }: FarmDetailPageProps) {
// // Unwrap the promised params using React.use() (experimental)
// const resolvedParams = React.use(params);
// const router = useRouter();
// const [farm, setFarm] = useState<Farm | null>(null);
// const [crops, setCrops] = useState<Crop[]>([]);
// const [isDialogOpen, setIsDialogOpen] = useState(false);
// const [isLoading, setIsLoading] = useState(true);
// const [error, setError] = useState<string | null>(null);
// const [activeFilter, setActiveFilter] = useState<string>("all");
// // Fetch farm details on initial render using the resolved params
// useEffect(() => {
// async function loadFarmDetails() {
// try {
// setIsLoading(true);
// setError(null);
// const { farm, crops } = await fetchFarmDetails(resolvedParams.farmId);
// setFarm(farm);
// setCrops(crops);
// } catch (err) {
// if (err instanceof Error) {
// if (err.message === "FARM_NOT_FOUND") {
// router.push("/not-found");
// return;
// }
// setError(err.message);
// } else {
// setError("An unknown error occurred");
// }
// } finally {
// setIsLoading(false);
// }
// }
// loadFarmDetails();
// }, [resolvedParams.farmId, router]);
// /**
// * Handles adding a new crop.
// */
// const handleAddCrop = async (data: Partial<Crop>) => {
// try {
// // Simulate API delay
// await new Promise((resolve) => setTimeout(resolve, 800));
// const newCrop: Crop = {
// id: Math.random().toString(36).substr(2, 9),
// farmId: farm!.id,
// name: data.name!,
// plantedDate: data.plantedDate!,
// status: data.status!,
// variety: data.variety || "Standard",
// area: data.area || "0 hectares",
// healthScore: data.status === "growing" ? 85 : 0,
// progress: data.status === "growing" ? 10 : 0,
// };
// setCrops((prev) => [newCrop, ...prev]);
// // Update the farm's crop count
// if (farm) {
// setFarm({ ...farm, crops: farm.crops + 1 });
// }
// setIsDialogOpen(false);
// } catch (err) {
// setError("Failed to add crop. Please try again.");
// }
// };
// // Filter crops based on the active filter
// const filteredCrops = crops.filter((crop) => activeFilter === "all" || crop.status === activeFilter);
// // Calculate crop counts grouped by status
// const cropCounts = {
// all: crops.length,
// growing: crops.filter((crop) => crop.status === "growing").length,
// planned: crops.filter((crop) => crop.status === "planned").length,
// harvested: crops.filter((crop) => crop.status === "harvested").length,
// };
// return (
// <div className="min-h-screen bg-background text-foreground">
// <div className="container max-w-7xl p-6 mx-auto">
// <div className="flex flex-col gap-6">
// {/* Breadcrumbs */}
// <nav className="flex items-center text-sm text-muted-foreground">
// <Button
// variant="link"
// className="p-0 h-auto font-normal text-muted-foreground"
// onClick={() => router.push("/")}>
// <Home className="h-3.5 w-3.5 mr-1" />
// Home
// </Button>
// <ChevronRight className="h-3.5 w-3.5 mx-1" />
// <Button
// variant="link"
// className="p-0 h-auto font-normal text-muted-foreground"
// onClick={() => router.push("/farms")}>
// Farms
// </Button>
// <ChevronRight className="h-3.5 w-3.5 mx-1" />
// <span className="text-foreground font-medium truncate">{farm?.name || "Farm Details"}</span>
// </nav>
// {/* Back button */}
// <Button
// variant="outline"
// size="sm"
// className="w-fit gap-2 text-muted-foreground"
// onClick={() => router.push("/farms")}>
// <ArrowLeft className="h-4 w-4" /> Back to Farms
// </Button>
// {/* Error state */}
// {error && (
// <Alert variant="destructive">
// <AlertTriangle className="h-4 w-4" />
// <AlertTitle>Error</AlertTitle>
// <AlertDescription>{error}</AlertDescription>
// </Alert>
// )}
// {/* Loading state */}
// {isLoading && (
// <div className="flex flex-col items-center justify-center py-12">
// <Loader2 className="h-8 w-8 text-green-600 animate-spin mb-4" />
// <p className="text-muted-foreground">Loading farm details...</p>
// </div>
// )}
// {/* Farm details */}
// {!isLoading && !error && farm && (
// <>
// <div className="grid gap-6 md:grid-cols-12">
// {/* Farm info card */}
// <Card className="md:col-span-8">
// <CardHeader className="pb-2">
// <div className="flex items-center justify-between">
// <Badge
// variant="outline"
// className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
// {farm.type}
// </Badge>
// <div className="flex items-center text-sm text-muted-foreground">
// <Calendar className="h-4 w-4 mr-1" />
// Created {farm.createdAt.toLocaleDateString()}
// </div>
// </div>
// <div className="flex items-start gap-4 mt-2">
// <div className="h-12 w-12 rounded-full bg-green-100 dark:bg-green-800 flex items-center justify-center">
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
// </div>
// <div>
// <h1 className="text-2xl font-bold">{farm.name}</h1>
// <div className="flex items-center text-muted-foreground mt-1">
// <MapPin className="h-4 w-4 mr-1" />
// {farm.location}
// </div>
// </div>
// </div>
// </CardHeader>
// <CardContent>
// <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-2">
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
// <p className="text-xs text-muted-foreground">Total Area</p>
// <p className="text-lg font-semibold">{farm.area}</p>
// </div>
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
// <p className="text-xs text-muted-foreground">Total Crops</p>
// <p className="text-lg font-semibold">{farm.crops}</p>
// </div>
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
// <p className="text-xs text-muted-foreground">Growing Crops</p>
// <p className="text-lg font-semibold">{cropCounts.growing}</p>
// </div>
// <div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
// <p className="text-xs text-muted-foreground">Harvested</p>
// <p className="text-lg font-semibold">{cropCounts.harvested}</p>
// </div>
// </div>
// </CardContent>
// </Card>
// {/* Weather card */}
// <Card className="md:col-span-4">
// <CardHeader>
// <CardTitle className="text-lg">Current Conditions</CardTitle>
// <CardDescription>Weather at your farm location</CardDescription>
// </CardHeader>
// <CardContent className="space-y-4">
// <div className="grid grid-cols-2 gap-4">
// <div className="flex items-start gap-2">
// <div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-900">
// <Sun className="h-4 w-4 text-orange-500 dark:text-orange-200" />
// </div>
// <div>
// <p className="text-sm font-medium text-muted-foreground">Temperature</p>
// <p className="text-xl font-semibold">{farm.weather?.temperature}°C</p>
// </div>
// </div>
// <div className="flex items-start gap-2">
// <div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900">
// <Droplets className="h-4 w-4 text-blue-500 dark:text-blue-200" />
// </div>
// <div>
// <p className="text-sm font-medium text-muted-foreground">Humidity</p>
// <p className="text-xl font-semibold">{farm.weather?.humidity}%</p>
// </div>
// </div>
// <div className="flex items-start gap-2">
// <div className="p-2 rounded-lg bg-yellow-50 dark:bg-yellow-900">
// <Sun className="h-4 w-4 text-yellow-500 dark:text-yellow-200" />
// </div>
// <div>
// <p className="text-sm font-medium text-muted-foreground">Sunlight</p>
// <p className="text-xl font-semibold">{farm.weather?.sunlight}%</p>
// </div>
// </div>
// <div className="flex items-start gap-2">
// <div className="p-2 rounded-lg bg-gray-50 dark:bg-gray-900">
// <Wind className="h-4 w-4 text-gray-500 dark:text-gray-300" />
// </div>
// <div>
// <p className="text-sm font-medium text-muted-foreground">Rainfall</p>
// <p className="text-xl font-semibold">{farm.weather?.rainfall}</p>
// </div>
// </div>
// </div>
// </CardContent>
// </Card>
// </div>
// {/* Crops section */}
// <div className="mt-4">
// <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
// <div>
// <h2 className="text-xl font-bold flex items-center">
// <LayoutGrid className="h-5 w-5 mr-2 text-green-600 dark:text-green-300" />
// Crops
// </h2>
// <p className="text-sm text-muted-foreground">Manage and monitor all crops in this farm</p>
// </div>
// <Button
// onClick={() => setIsDialogOpen(true)}
// className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto">
// <Plus className="h-4 w-4" />
// Add New Crop
// </Button>
// </div>
// <Tabs defaultValue="all" className="mt-6">
// <TabsList>
// <TabsTrigger value="all" onClick={() => setActiveFilter("all")}>
// All Crops ({cropCounts.all})
// </TabsTrigger>
// <TabsTrigger value="growing" onClick={() => setActiveFilter("growing")}>
// Growing ({cropCounts.growing})
// </TabsTrigger>
// <TabsTrigger value="planned" onClick={() => setActiveFilter("planned")}>
// Planned ({cropCounts.planned})
// </TabsTrigger>
// <TabsTrigger value="harvested" onClick={() => setActiveFilter("harvested")}>
// Harvested ({cropCounts.harvested})
// </TabsTrigger>
// </TabsList>
// <TabsContent value="all" className="mt-6">
// {filteredCrops.length === 0 ? (
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
// <div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
// </div>
// <h3 className="text-xl font-medium mb-2">No crops found</h3>
// <p className="text-muted-foreground text-center max-w-md mb-6">
// {activeFilter === "all"
// ? "You haven't added any crops to this farm yet."
// : `No ${activeFilter} crops found. Try a different filter.`}
// </p>
// <Button onClick={() => setIsDialogOpen(true)} className="gap-2">
// <Plus className="h-4 w-4" />
// Add your first crop
// </Button>
// </div>
// ) : (
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
// <AnimatePresence>
// {filteredCrops.map((crop, index) => (
// <motion.div
// key={crop.id}
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// exit={{ opacity: 0, y: -20 }}
// transition={{ duration: 0.2, delay: index * 0.05 }}>
// <CropCard
// crop={crop}
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
// />
// </motion.div>
// ))}
// </AnimatePresence>
// </div>
// )}
// </TabsContent>
// {/* Growing tab */}
// <TabsContent value="growing" className="mt-6">
// {filteredCrops.length === 0 ? (
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
// <div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
// <Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
// </div>
// <h3 className="text-xl font-medium mb-2">No growing crops</h3>
// <p className="text-muted-foreground text-center max-w-md mb-6">
// You don't have any growing crops in this farm yet.
// </p>
// <Button onClick={() => setIsDialogOpen(true)} className="gap-2">
// <Plus className="h-4 w-4" />
// Add a growing crop
// </Button>
// </div>
// ) : (
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
// <AnimatePresence>
// {filteredCrops.map((crop, index) => (
// <motion.div
// key={crop.id}
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// exit={{ opacity: 0, y: -20 }}
// transition={{ duration: 0.2, delay: index * 0.05 }}>
// <CropCard
// crop={crop}
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
// />
// </motion.div>
// ))}
// </AnimatePresence>
// </div>
// )}
// </TabsContent>
// {/* Planned tab */}
// <TabsContent value="planned" className="mt-6">
// {filteredCrops.length === 0 ? (
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
// <h3 className="text-xl font-medium mb-2">No planned crops</h3>
// <p className="text-muted-foreground text-center max-w-md mb-6">
// You don't have any planned crops in this farm yet.
// </p>
// <Button onClick={() => setIsDialogOpen(true)} className="gap-2">
// <Plus className="h-4 w-4" />
// Plan a new crop
// </Button>
// </div>
// ) : (
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
// <AnimatePresence>
// {filteredCrops.map((crop, index) => (
// <motion.div
// key={crop.id}
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// exit={{ opacity: 0, y: -20 }}
// transition={{ duration: 0.2, delay: index * 0.05 }}>
// <CropCard
// crop={crop}
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
// />
// </motion.div>
// ))}
// </AnimatePresence>
// </div>
// )}
// </TabsContent>
// {/* Harvested tab */}
// <TabsContent value="harvested" className="mt-6">
// {filteredCrops.length === 0 ? (
// <div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
// <h3 className="text-xl font-medium mb-2">No harvested crops</h3>
// <p className="text-muted-foreground text-center max-w-md mb-6">
// You don't have any harvested crops in this farm yet.
// </p>
// <Button onClick={() => setActiveFilter("all")} className="gap-2">
// View all crops
// </Button>
// </div>
// ) : (
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
// <AnimatePresence>
// {filteredCrops.map((crop, index) => (
// <motion.div
// key={crop.id}
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// exit={{ opacity: 0, y: -20 }}
// transition={{ duration: 0.2, delay: index * 0.05 }}>
// <CropCard
// crop={crop}
// onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
// />
// </motion.div>
// ))}
// </AnimatePresence>
// </div>
// )}
// </TabsContent>
// </Tabs>
// </div>
// </>
// )}
// </div>
// </div>
// {/* Add Crop Dialog */}
// <CropDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} onSubmit={handleAddCrop} />
// </div>
// );
// }
export default function FarmDetailPage({ params }: FarmDetailPageProps) {
// Unwrap the promised params using React.use() (experimental)
const resolvedParams = React.use(params);
const router = useRouter();
const [farm, setFarm] = useState<Farm | null>(null);
const [crops, setCrops] = useState<Crop[]>([]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeFilter, setActiveFilter] = useState<string>("all");
// Fetch farm details on initial render using the resolved params
useEffect(() => {
async function loadFarmDetails() {
try {
setIsLoading(true);
setError(null);
const { farm, crops } = await fetchFarmDetails(resolvedParams.farmId);
setFarm(farm);
setCrops(crops);
} catch (err) {
if (err instanceof Error) {
if (err.message === "FARM_NOT_FOUND") {
router.push("/not-found");
return;
}
setError(err.message);
} else {
setError("An unknown error occurred");
}
} finally {
setIsLoading(false);
}
}
loadFarmDetails();
}, [resolvedParams.farmId, router]);
/**
* Handles adding a new crop.
*/
const handleAddCrop = async (data: Partial<Crop>) => {
try {
// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 800));
const newCrop: Crop = {
id: Math.random().toString(36).substr(2, 9),
farmId: farm!.id,
name: data.name!,
plantedDate: data.plantedDate!,
status: data.status!,
variety: data.variety || "Standard",
area: data.area || "0 hectares",
healthScore: data.status === "growing" ? 85 : 0,
progress: data.status === "growing" ? 10 : 0,
};
setCrops((prev) => [newCrop, ...prev]);
// Update the farm's crop count
if (farm) {
setFarm({ ...farm, crops: farm.crops + 1 });
}
setIsDialogOpen(false);
} catch (err) {
setError("Failed to add crop. Please try again.");
}
};
// Filter crops based on the active filter
const filteredCrops = crops.filter((crop) => activeFilter === "all" || crop.status === activeFilter);
// Calculate crop counts grouped by status
const cropCounts = {
all: crops.length,
growing: crops.filter((crop) => crop.status === "growing").length,
planned: crops.filter((crop) => crop.status === "planned").length,
harvested: crops.filter((crop) => crop.status === "harvested").length,
};
return (
<div className="min-h-screen bg-background text-foreground">
<div className="container max-w-7xl p-6 mx-auto">
<div className="flex flex-col gap-6">
{/* Breadcrumbs */}
<nav className="flex items-center text-sm text-muted-foreground">
<Button
variant="link"
className="p-0 h-auto font-normal text-muted-foreground"
onClick={() => router.push("/")}>
<Home className="h-3.5 w-3.5 mr-1" />
Home
</Button>
<ChevronRight className="h-3.5 w-3.5 mx-1" />
<Button
variant="link"
className="p-0 h-auto font-normal text-muted-foreground"
onClick={() => router.push("/farms")}>
Farms
</Button>
<ChevronRight className="h-3.5 w-3.5 mx-1" />
<span className="text-foreground font-medium truncate">{farm?.name || "Farm Details"}</span>
</nav>
{/* Back button */}
<Button
variant="outline"
size="sm"
className="w-fit gap-2 text-muted-foreground"
onClick={() => router.push("/farms")}>
<ArrowLeft className="h-4 w-4" /> Back to Farms
</Button>
{/* Error state */}
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Loading state */}
{isLoading && (
<div className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 text-green-600 animate-spin mb-4" />
<p className="text-muted-foreground">Loading farm details...</p>
</div>
)}
{/* Farm details */}
{!isLoading && !error && farm && (
<>
<div className="grid gap-6 md:grid-cols-12">
{/* Farm info card */}
<Card className="md:col-span-8">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<Badge
variant="outline"
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
{farm.type}
</Badge>
<div className="flex items-center text-sm text-muted-foreground">
<Calendar className="h-4 w-4 mr-1" />
Created {farm.createdAt.toLocaleDateString()}
</div>
</div>
<div className="flex items-start gap-4 mt-2">
<div className="h-12 w-12 rounded-full bg-green-100 dark:bg-green-800 flex items-center justify-center">
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
</div>
<div>
<h1 className="text-2xl font-bold">{farm.name}</h1>
<div className="flex items-center text-muted-foreground mt-1">
<MapPin className="h-4 w-4 mr-1" />
{farm.location}
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-2">
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
<p className="text-xs text-muted-foreground">Total Area</p>
<p className="text-lg font-semibold">{farm.area}</p>
</div>
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
<p className="text-xs text-muted-foreground">Total Crops</p>
<p className="text-lg font-semibold">{farm.crops}</p>
</div>
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
<p className="text-xs text-muted-foreground">Growing Crops</p>
<p className="text-lg font-semibold">{cropCounts.growing}</p>
</div>
<div className="bg-muted/30 dark:bg-muted/20 rounded-lg p-3">
<p className="text-xs text-muted-foreground">Harvested</p>
<p className="text-lg font-semibold">{cropCounts.harvested}</p>
</div>
</div>
</CardContent>
</Card>
{/* Weather card */}
<Card className="md:col-span-4">
<CardHeader>
<CardTitle className="text-lg">Current Conditions</CardTitle>
<CardDescription>Weather at your farm location</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-start gap-2">
<div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-900">
<Sun className="h-4 w-4 text-orange-500 dark:text-orange-200" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Temperature</p>
<p className="text-xl font-semibold">{farm.weather?.temperature}°C</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900">
<Droplets className="h-4 w-4 text-blue-500 dark:text-blue-200" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Humidity</p>
<p className="text-xl font-semibold">{farm.weather?.humidity}%</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="p-2 rounded-lg bg-yellow-50 dark:bg-yellow-900">
<Sun className="h-4 w-4 text-yellow-500 dark:text-yellow-200" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Sunlight</p>
<p className="text-xl font-semibold">{farm.weather?.sunlight}%</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="p-2 rounded-lg bg-gray-50 dark:bg-gray-900">
<Wind className="h-4 w-4 text-gray-500 dark:text-gray-300" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Rainfall</p>
<p className="text-xl font-semibold">{farm.weather?.rainfall}</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Crops section */}
<div className="mt-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<div>
<h2 className="text-xl font-bold flex items-center">
<LayoutGrid className="h-5 w-5 mr-2 text-green-600 dark:text-green-300" />
Crops
</h2>
<p className="text-sm text-muted-foreground">Manage and monitor all crops in this farm</p>
</div>
<Button
onClick={() => setIsDialogOpen(true)}
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto">
<Plus className="h-4 w-4" />
Add New Crop
</Button>
</div>
<Tabs defaultValue="all" className="mt-6">
<TabsList>
<TabsTrigger value="all" onClick={() => setActiveFilter("all")}>
All Crops ({cropCounts.all})
</TabsTrigger>
<TabsTrigger value="growing" onClick={() => setActiveFilter("growing")}>
Growing ({cropCounts.growing})
</TabsTrigger>
<TabsTrigger value="planned" onClick={() => setActiveFilter("planned")}>
Planned ({cropCounts.planned})
</TabsTrigger>
<TabsTrigger value="harvested" onClick={() => setActiveFilter("harvested")}>
Harvested ({cropCounts.harvested})
</TabsTrigger>
</TabsList>
<TabsContent value="all" className="mt-6">
{filteredCrops.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
<div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
</div>
<h3 className="text-xl font-medium mb-2">No crops found</h3>
<p className="text-muted-foreground text-center max-w-md mb-6">
{activeFilter === "all"
? "You haven't added any crops to this farm yet."
: `No ${activeFilter} crops found. Try a different filter.`}
</p>
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
<Plus className="h-4 w-4" />
Add your first crop
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<AnimatePresence>
{filteredCrops.map((crop, index) => (
<motion.div
key={crop.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, delay: index * 0.05 }}>
<CropCard
crop={crop}
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
/>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</TabsContent>
{/* Growing tab */}
<TabsContent value="growing" className="mt-6">
{filteredCrops.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
<div className="bg-green-100 dark:bg-green-900 p-3 rounded-full mb-4">
<Sprout className="h-6 w-6 text-green-600 dark:text-green-300" />
</div>
<h3 className="text-xl font-medium mb-2">No growing crops</h3>
<p className="text-muted-foreground text-center max-w-md mb-6">
You don't have any growing crops in this farm yet.
</p>
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
<Plus className="h-4 w-4" />
Add a growing crop
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<AnimatePresence>
{filteredCrops.map((crop, index) => (
<motion.div
key={crop.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, delay: index * 0.05 }}>
<CropCard
crop={crop}
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
/>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</TabsContent>
{/* Planned tab */}
<TabsContent value="planned" className="mt-6">
{filteredCrops.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
<h3 className="text-xl font-medium mb-2">No planned crops</h3>
<p className="text-muted-foreground text-center max-w-md mb-6">
You don't have any planned crops in this farm yet.
</p>
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
<Plus className="h-4 w-4" />
Plan a new crop
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<AnimatePresence>
{filteredCrops.map((crop, index) => (
<motion.div
key={crop.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, delay: index * 0.05 }}>
<CropCard
crop={crop}
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
/>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</TabsContent>
{/* Harvested tab */}
<TabsContent value="harvested" className="mt-6">
{filteredCrops.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 dark:bg-muted/30 rounded-lg border border-dashed">
<h3 className="text-xl font-medium mb-2">No harvested crops</h3>
<p className="text-muted-foreground text-center max-w-md mb-6">
You don't have any harvested crops in this farm yet.
</p>
<Button onClick={() => setActiveFilter("all")} className="gap-2">
View all crops
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<AnimatePresence>
{filteredCrops.map((crop, index) => (
<motion.div
key={crop.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, delay: index * 0.05 }}>
<CropCard
crop={crop}
onClick={() => router.push(`/farms/${crop.farmId}/crops/${crop.id}`)}
/>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</TabsContent>
</Tabs>
</div>
</>
)}
</div>
</div>
{/* Add Crop Dialog */}
<CropDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} onSubmit={handleAddCrop} />
</div>
);
return <div>hello</div>;
}

View File

@ -10,10 +10,12 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For
import { useState } from "react";
import { Loader2 } from "lucide-react";
import type { Farm } from "@/types";
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
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"),
latitude: z.number().min(-90, "Invalid latitude").max(90, "Invalid latitude"),
longitude: z.number().min(-180, "Invalid longitude").max(180, "Invalid longitude"),
type: z.string().min(1, "Please select a farm type"),
area: z.string().optional(),
});
@ -30,7 +32,8 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
resolver: zodResolver(farmFormSchema),
defaultValues: {
name: "",
location: "",
latitude: 0,
longitude: 0,
type: "",
area: "",
},
@ -39,7 +42,14 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
const handleSubmit = async (values: z.infer<typeof farmFormSchema>) => {
try {
setIsSubmitting(true);
await onSubmit(values);
const farmData: Partial<Farm> = {
Name: values.name,
Lat: values.latitude,
Lon: values.longitude,
FarmType: values.type,
TotalSize: values.area,
};
await onSubmit(farmData);
form.reset();
} catch (error) {
console.error("Error submitting form:", error);
@ -48,95 +58,127 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
}
};
const handleAreaSelected = (coordinates: { lat: number; lng: number }[]) => {
if (coordinates.length > 0) {
const { lat, lng } = coordinates[0];
form.setValue("latitude", lat);
form.setValue("longitude", lng);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Farm Name</FormLabel>
<FormControl>
<Input placeholder="Enter farm name" {...field} />
</FormControl>
<FormDescription>This is your farm's display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col lg:flex-row gap-6">
{/* Form Section */}
<div className="flex-1">
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Farm Name</FormLabel>
<FormControl>
<Input placeholder="Enter farm name" {...field} />
</FormControl>
<FormDescription>This is your farm&apos;s display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Location</FormLabel>
<FormControl>
<Input placeholder="Enter farm location" {...field} />
</FormControl>
<FormDescription>City, region or specific address</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="latitude"
render={({ field }) => (
<FormItem>
<FormLabel>Latitude</FormLabel>
<FormControl>
<Input placeholder="Latitude" {...field} disabled />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Farm Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select farm type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="durian">Durian</SelectItem>
<SelectItem value="mango">Mango</SelectItem>
<SelectItem value="rice">Rice</SelectItem>
<SelectItem value="mixed">Mixed Crops</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="longitude"
render={({ field }) => (
<FormItem>
<FormLabel>Longitude</FormLabel>
<FormControl>
<Input placeholder="Longitude" {...field} disabled />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="area"
render={({ field }) => (
<FormItem>
<FormLabel>Total Area (optional)</FormLabel>
<FormControl>
<Input placeholder="e.g., 10 hectares" {...field} />
</FormControl>
<FormDescription>The total size of your farm</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Farm Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select farm type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="durian">Durian</SelectItem>
<SelectItem value="mango">Mango</SelectItem>
<SelectItem value="rice">Rice</SelectItem>
<SelectItem value="mixed">Mixed Crops</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting} className="bg-green-600 hover:bg-green-700">
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Farm"
)}
</Button>
</div>
</form>
</Form>
<FormField
control={form.control}
name="area"
render={({ field }) => (
<FormItem>
<FormLabel>Total Area (optional)</FormLabel>
<FormControl>
<Input placeholder="e.g., 10 hectares" {...field} />
</FormControl>
<FormDescription>The total size of your farm</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting} className="bg-green-600 hover:bg-green-700">
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Farm"
)}
</Button>
</div>
</form>
</Form>
</div>
{/* Map Section */}
<div className="flex-1">
<FormLabel>Farm Location</FormLabel>
<GoogleMapWithDrawing onAreaSelected={handleAreaSelected} />
</div>
</div>
);
}

View File

@ -40,7 +40,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
year: "numeric",
month: "short",
day: "numeric",
}).format(farm.createdAt);
}).format(new Date(farm.CreatedAt));
return (
<Card className={cardClasses} onClick={onClick}>
@ -49,7 +49,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
<Badge
variant="outline"
className="capitalize bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border-green-200">
{farm.type}
{farm.FarmType}
</Badge>
<div className="flex items-center text-xs text-muted-foreground">
<CalendarDays className="h-3 w-3 mr-1" />
@ -63,15 +63,15 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
<Sprout className="h-5 w-5 text-green-600" />
</div>
<div>
<h3 className="text-xl font-medium mb-1 truncate">{farm.name}</h3>
<h3 className="text-xl font-medium mb-1 truncate">{farm.Name}</h3>
<div className="flex items-center text-sm text-muted-foreground mb-2">
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
<span className="truncate">{farm.location}</span>
<span className="truncate">{farm.Lat}</span>
</div>
<div className="grid grid-cols-2 gap-2 mt-3">
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
<p className="text-xs text-muted-foreground">Area</p>
<p className="font-medium">{farm.area}</p>
<p className="font-medium">{farm.TotalSize}</p>
</div>
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
<p className="text-xs text-muted-foreground">Crops</p>

View File

@ -54,26 +54,38 @@ export default function FarmSetupPage() {
},
});
// export interface Farm {
// CreatedAt: string;
// FarmType: string;
// Lat: number;
// Lon: number;
// Name: string;
// OwnerID: string;
// TotalSize: string;
// UUID: string;
// UpdatedAt: string;
// }
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()))
(activeFilter === "all" || farm.FarmType === activeFilter) &&
(farm.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
// farm.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
farm.FarmType.toLowerCase().includes(searchQuery.toLowerCase()))
)
.sort((a, b) => {
if (sortOrder === "newest") {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
return new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime();
} else if (sortOrder === "oldest") {
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
return new Date(a.CreatedAt).getTime() - new Date(b.CreatedAt).getTime();
} else {
return a.name.localeCompare(b.name);
return a.Name.localeCompare(b.Name);
}
});
// Get distinct farm types.
const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.type))];
const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.FarmType))];
const handleAddFarm = async (data: Partial<Farm>) => {
await mutation.mutateAsync(data);
@ -188,7 +200,7 @@ export default function FarmSetupPage() {
</p>
) : (
<p className="text-muted-foreground text-center max-w-md mb-6">
You haven't added any farms yet. Get started by adding your first farm.
You haven&apos;t added any farms yet. Get started by adding your first farm.
</p>
)}
<Button
@ -226,13 +238,13 @@ export default function FarmSetupPage() {
</motion.div>
{filteredAndSortedFarms.map((farm, index) => (
<motion.div
key={farm.id}
key={farm.UUID}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
className="col-span-1">
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.id}`)} />
<FarmCard variant="farm" farm={farm} onClick={() => router.push(`/farms/${farm.UUID}`)} />
</motion.div>
))}
</AnimatePresence>

View File

@ -8,6 +8,7 @@ import DynamicBreadcrumb from "./dynamic-breadcrumb";
import { extractRoute } from "@/lib/utils";
import { usePathname } from "next/navigation";
import { Toaster } from "@/components/ui/sonner";
import { useForm, FormProvider } from "react-hook-form";
export default function AppLayout({
children,
@ -16,21 +17,24 @@ export default function AppLayout({
}>) {
const pathname = usePathname();
const currentPathname = extractRoute(pathname);
const form = useForm();
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<ThemeToggle />
<Separator orientation="vertical" className="mr-2 h-4" />
<DynamicBreadcrumb pathname={currentPathname} />
</div>
</header>
{children}
<Toaster />
<FormProvider {...form}>
<header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<ThemeToggle />
<Separator orientation="vertical" className="mr-2 h-4" />
<DynamicBreadcrumb pathname={currentPathname} />
</div>
</header>
{children}
<Toaster />
</FormProvider>
</SidebarInset>
</SidebarProvider>
);

File diff suppressed because it is too large Load Diff

View File

@ -32,21 +32,33 @@ export interface CropAnalytics {
}
export interface Farm {
id: string;
name: string;
location: string;
type: string;
createdAt: Date;
area?: string;
crops: number;
weather?: {
temperature: number;
humidity: number;
rainfall: string;
sunlight: number;
};
CreatedAt: Date;
FarmType: string;
Lat: number;
Lon: number;
Name: string;
OwnerID: string;
TotalSize: string;
UUID: string;
UpdatedAt: Date;
}
// export interface Farm {
// id: string;
// name: string;
// location: string;
// type: string;
// createdAt: Date;
// area?: string;
// crops: number;
// weather?: {
// temperature: number;
// humidity: number;
// rainfall: string;
// sunlight: number;
// };
// }
export interface User {
ID: number;
UUID: string;