mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
feat: use react query to fetch farm
This commit is contained in:
parent
54bc4c33a3
commit
ba4c2ce1b4
@ -1,13 +1,17 @@
|
|||||||
|
import axiosInstance from "./config";
|
||||||
import type { Crop, CropAnalytics, Farm } from "@/types";
|
import type { Crop, CropAnalytics, Farm } from "@/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch mock crop data by id.
|
* Fetch a specific crop by id using axios.
|
||||||
* @param id - The crop identifier.
|
* Falls back to dummy data on error.
|
||||||
* @returns A promise that resolves to a Crop object.
|
|
||||||
*/
|
*/
|
||||||
export async function fetchCropById(id: string): Promise<Crop> {
|
export async function fetchCropById(id: string): Promise<Crop> {
|
||||||
// Simulate an API delay if needed.
|
try {
|
||||||
return Promise.resolve({
|
const response = await axiosInstance.get<Crop>(`/api/crops/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback dummy data
|
||||||
|
return {
|
||||||
id,
|
id,
|
||||||
farmId: "1",
|
farmId: "1",
|
||||||
name: "Monthong Durian",
|
name: "Monthong Durian",
|
||||||
@ -17,16 +21,20 @@ export async function fetchCropById(id: string): Promise<Crop> {
|
|||||||
expectedHarvest: new Date("2024-07-15"),
|
expectedHarvest: new Date("2024-07-15"),
|
||||||
area: "2.5 hectares",
|
area: "2.5 hectares",
|
||||||
healthScore: 85,
|
healthScore: 85,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch mock crop analytics data by crop id.
|
* Fetch crop analytics by crop id using axios.
|
||||||
* @param id - The crop identifier.
|
* Returns dummy analytics if the API call fails.
|
||||||
* @returns A promise that resolves to a CropAnalytics object.
|
|
||||||
*/
|
*/
|
||||||
export async function fetchAnalyticsByCropId(id: string): Promise<CropAnalytics> {
|
export async function fetchAnalyticsByCropId(id: string): Promise<CropAnalytics> {
|
||||||
return Promise.resolve({
|
try {
|
||||||
|
const response = await axiosInstance.get<CropAnalytics>(`/api/crops/${id}/analytics`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
cropId: id,
|
cropId: id,
|
||||||
growthProgress: 45,
|
growthProgress: 45,
|
||||||
humidity: 75,
|
humidity: 75,
|
||||||
@ -44,24 +52,23 @@ export async function fetchAnalyticsByCropId(id: string): Promise<CropAnalytics>
|
|||||||
phosphorus: 65,
|
phosphorus: 65,
|
||||||
potassium: 75,
|
potassium: 75,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulates an API call to fetch farms.
|
* Fetch an array of farms using axios.
|
||||||
* Introduces a delay and a random error to emulate network conditions.
|
* Simulates a delay and a random error; returns dummy data if the API is unavailable.
|
||||||
*
|
|
||||||
* @returns A promise that resolves to an array of Farm objects.
|
|
||||||
*/
|
*/
|
||||||
export async function fetchFarms(): Promise<Farm[]> {
|
export async function fetchFarms(): Promise<Farm[]> {
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
// Simulate a random error (roughly 1 in 10 chance)
|
try {
|
||||||
if (Math.random() < 0.1) {
|
const response = await axiosInstance.get<Farm[]>("/api/farms");
|
||||||
throw new Error("Failed to fetch farms. Please try again later.");
|
return response.data;
|
||||||
}
|
} catch (error) {
|
||||||
|
// Optionally, you could simulate a random error here. For now we return fallback data.
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
@ -92,25 +99,43 @@ export async function fetchFarms(): Promise<Farm[]> {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulates an API call to fetch farm details along with its crops.
|
* Simulates creating a new farm.
|
||||||
* This function adds a delay and randomly generates an error to mimic real-world conditions.
|
* Waits for 800ms and then uses dummy data.
|
||||||
*
|
*/
|
||||||
* @param farmId - The unique identifier of the farm to retrieve.
|
export async function createFarm(data: Partial<Farm>): Promise<Farm> {
|
||||||
* @returns A promise resolving with an object that contains the farm details and an array of crops.
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
* @throws An error if the simulated network call fails or if the farm is not found.
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
*/
|
*/
|
||||||
export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; crops: Crop[] }> {
|
export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; crops: Crop[] }> {
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||||
|
|
||||||
// Randomly simulate an error (about 1 in 10 chance)
|
try {
|
||||||
if (Math.random() < 0.1) {
|
const response = await axiosInstance.get<{ farm: Farm; crops: Crop[] }>(`/api/farms/${farmId}`);
|
||||||
throw new Error("Failed to fetch farm details. Please try again later.");
|
return response.data;
|
||||||
}
|
} catch (error) {
|
||||||
|
// If the given farmId is "999", simulate a not found error.
|
||||||
// Simulate a not found error if the given farmId is "999"
|
|
||||||
if (farmId === "999") {
|
if (farmId === "999") {
|
||||||
throw new Error("FARM_NOT_FOUND");
|
throw new Error("FARM_NOT_FOUND");
|
||||||
}
|
}
|
||||||
@ -123,6 +148,7 @@ export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; cr
|
|||||||
createdAt: new Date("2023-01-15"),
|
createdAt: new Date("2023-01-15"),
|
||||||
area: "12.5 hectares",
|
area: "12.5 hectares",
|
||||||
crops: 3,
|
crops: 3,
|
||||||
|
// Additional details such as weather can be included if needed.
|
||||||
weather: {
|
weather: {
|
||||||
temperature: 28,
|
temperature: 28,
|
||||||
humidity: 75,
|
humidity: 75,
|
||||||
@ -169,3 +195,4 @@ export async function fetchFarmDetails(farmId: string): Promise<{ farm: Farm; cr
|
|||||||
|
|
||||||
return { farm, crops };
|
return { farm, crops };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const farmFormSchema = z.object({
|
|||||||
area: z.string().optional(),
|
area: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface AddFarmFormProps {
|
export interface AddFarmFormProps {
|
||||||
onSubmit: (data: Partial<Farm>) => Promise<void>;
|
onSubmit: (data: Partial<Farm>) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
<Sprout className="h-5 w-5 text-green-600" />
|
<Sprout className="h-5 w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-medium mb-1">{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">
|
<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" />
|
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||||
<span className="truncate">{farm.location}</span>
|
<span className="truncate">{farm.location}</span>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Search, Plus, Filter, SlidersHorizontal, Leaf, Calendar, AlertTriangle, Loader2 } from "lucide-react";
|
import { Search, Plus, Filter, SlidersHorizontal, Leaf, Calendar, AlertTriangle, Loader2 } from "lucide-react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@ -23,70 +24,37 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|||||||
import { FarmCard } from "./farm-card";
|
import { FarmCard } from "./farm-card";
|
||||||
import { AddFarmForm } from "./add-farm-form";
|
import { AddFarmForm } from "./add-farm-form";
|
||||||
import type { Farm } from "@/types";
|
import type { Farm } from "@/types";
|
||||||
import { fetchFarms } from "@/api/farm";
|
import { fetchFarms, createFarm } from "@/api/farm";
|
||||||
|
|
||||||
/**
|
|
||||||
* FarmSetupPage component allows users to search, filter, sort, and add farms.
|
|
||||||
*/
|
|
||||||
export default function FarmSetupPage() {
|
export default function FarmSetupPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Component state
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [farms, setFarms] = useState<Farm[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [activeFilter, setActiveFilter] = useState<string>("all");
|
const [activeFilter, setActiveFilter] = useState<string>("all");
|
||||||
const [sortOrder, setSortOrder] = useState<"newest" | "oldest" | "alphabetical">("newest");
|
const [sortOrder, setSortOrder] = useState<"newest" | "oldest" | "alphabetical">("newest");
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Load farms when the component mounts.
|
const {
|
||||||
useEffect(() => {
|
data: farms,
|
||||||
async function loadFarms() {
|
isLoading,
|
||||||
try {
|
isError,
|
||||||
setIsLoading(true);
|
error,
|
||||||
setError(null);
|
} = useQuery<Farm[]>({
|
||||||
const data = await fetchFarms();
|
queryKey: ["farms"],
|
||||||
setFarms(data);
|
queryFn: fetchFarms,
|
||||||
} catch (err) {
|
staleTime: 60 * 1000,
|
||||||
setError(err instanceof Error ? err.message : "An unknown error occurred");
|
});
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFarms();
|
const mutation = useMutation({
|
||||||
}, []);
|
mutationFn: (data: Partial<Farm>) => createFarm(data),
|
||||||
|
onSuccess: () => {
|
||||||
/**
|
queryClient.invalidateQueries({ queryKey: ["farms"] });
|
||||||
* Handles adding a new farm.
|
|
||||||
*
|
|
||||||
* @param data - Partial Farm data from the form.
|
|
||||||
*/
|
|
||||||
const handleAddFarm = async (data: Partial<Farm>) => {
|
|
||||||
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);
|
setIsDialogOpen(false);
|
||||||
} catch (err) {
|
},
|
||||||
setError("Failed to add farm. Please try again.");
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter and sort farms based on the current state.
|
const filteredAndSortedFarms = (farms || [])
|
||||||
const filteredAndSortedFarms = farms
|
|
||||||
.filter(
|
.filter(
|
||||||
(farm) =>
|
(farm) =>
|
||||||
(activeFilter === "all" || farm.type === activeFilter) &&
|
(activeFilter === "all" || farm.type === activeFilter) &&
|
||||||
@ -96,16 +64,20 @@ export default function FarmSetupPage() {
|
|||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (sortOrder === "newest") {
|
if (sortOrder === "newest") {
|
||||||
return b.createdAt.getTime() - a.createdAt.getTime();
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||||
} else if (sortOrder === "oldest") {
|
} else if (sortOrder === "oldest") {
|
||||||
return a.createdAt.getTime() - b.createdAt.getTime();
|
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
} else {
|
} else {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get available farm types for filters.
|
// Get distinct farm types.
|
||||||
const farmTypes = ["all", ...new Set(farms.map((farm) => farm.type))];
|
const farmTypes = ["all", ...new Set((farms || []).map((farm) => farm.type))];
|
||||||
|
|
||||||
|
const handleAddFarm = async (data: Partial<Farm>) => {
|
||||||
|
await mutation.mutateAsync(data);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b">
|
<div className="min-h-screen bg-gradient-to-b">
|
||||||
@ -187,11 +159,11 @@ export default function FarmSetupPage() {
|
|||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
|
|
||||||
{/* Error state */}
|
{/* Error state */}
|
||||||
{error && (
|
{isError && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{error}</AlertDescription>
|
<AlertDescription>{(error as Error)?.message}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -204,7 +176,7 @@ export default function FarmSetupPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
{!isLoading && !error && filteredAndSortedFarms.length === 0 && (
|
{!isLoading && !isError && filteredAndSortedFarms.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 rounded-lg border border-dashed">
|
<div className="flex flex-col items-center justify-center py-12 bg-muted/20 rounded-lg border border-dashed">
|
||||||
<div className="bg-green-100 p-3 rounded-full mb-4">
|
<div className="bg-green-100 p-3 rounded-full mb-4">
|
||||||
<Leaf className="h-6 w-6 text-green-600" />
|
<Leaf className="h-6 w-6 text-green-600" />
|
||||||
@ -223,7 +195,7 @@ export default function FarmSetupPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setActiveFilter("all");
|
setActiveFilter("all");
|
||||||
if (!farms.length) {
|
if (!farms || farms.length === 0) {
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -241,7 +213,7 @@ export default function FarmSetupPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Grid of farm cards */}
|
{/* Grid of farm cards */}
|
||||||
{!isLoading && !error && filteredAndSortedFarms.length > 0 && (
|
{!isLoading && !isError && filteredAndSortedFarms.length > 0 && (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -252,7 +224,6 @@ export default function FarmSetupPage() {
|
|||||||
className="col-span-1">
|
className="col-span-1">
|
||||||
<FarmCard variant="add" onClick={() => setIsDialogOpen(true)} />
|
<FarmCard variant="add" onClick={() => setIsDialogOpen(true)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{filteredAndSortedFarms.map((farm, index) => (
|
{filteredAndSortedFarms.map((farm, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={farm.id}
|
key={farm.id}
|
||||||
@ -285,9 +256,7 @@ export default function FarmSetupPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper component to render the Check icon.
|
* A helper component for the Check icon.
|
||||||
*
|
|
||||||
* @param props - Optional className for custom styling.
|
|
||||||
*/
|
*/
|
||||||
function Check({ className }: { className?: string }) {
|
function Check({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -170,7 +170,7 @@ export default function KnowledgeHubPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="p-4 pt-0 flex justify-between items-center">
|
<CardFooter className="p-4 pt-0 flex justify-between items-center">
|
||||||
<div className="text-sm text-muted-foreground">By {blog.author}</div>
|
<div className="text-sm text-muted-foreground">By {blog.author}</div>
|
||||||
<Link href={`/blog/${blog.id}`}>
|
<Link href={`/hub/${blog.id}`}>
|
||||||
<Button variant="ghost" size="sm" className="gap-1">
|
<Button variant="ghost" size="sm" className="gap-1">
|
||||||
Read <ChevronRight className="h-4 w-4" />
|
Read <ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user