diff --git a/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx b/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx new file mode 100644 index 0000000..ce33e21 --- /dev/null +++ b/frontend/app/(sidebar)/farms/[farmId]/add-crop-form.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +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, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Crop } from "@/types"; +import { cropFormSchema } from "@/schemas/form.schema"; + +interface AddCropFormProps { + onSubmit: (data: Partial) => Promise; + onCancel: () => void; +} + +export function AddCropForm({ onSubmit, onCancel }: AddCropFormProps) { + const form = useForm>({ + resolver: zodResolver(cropFormSchema), + defaultValues: { + name: "", + plantedDate: "", + status: "planned", + }, + }); + + const handleSubmit = (values: z.infer) => { + onSubmit({ + ...values, + plantedDate: new Date(values.plantedDate), + }); + }; + + return ( +
+ + ( + + Crop Name + + + + + + )} + /> + + ( + + Planted Date + + + + + + )} + /> + + ( + + Status + + + + )} + /> + +
+ + +
+ + + ); +} diff --git a/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx b/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx new file mode 100644 index 0000000..6d9dd72 --- /dev/null +++ b/frontend/app/(sidebar)/farms/[farmId]/crop-card.tsx @@ -0,0 +1,37 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Sprout, Calendar } from "lucide-react"; +import { Crop } from "@/types"; + +interface CropCardProps { + crop: Crop; +} + +export function CropCard({ crop }: CropCardProps) { + const statusColors = { + growing: "text-green-500", + harvested: "text-yellow-500", + planned: "text-blue-500", + }; + + return ( + + +
+
+ +
+ {crop.status} +
+
+ +
+

{crop.name}

+
+ +

Planted: {crop.plantedDate.toLocaleDateString()}

+
+
+
+
+ ); +} diff --git a/frontend/app/(sidebar)/farms/[farmId]/page.tsx b/frontend/app/(sidebar)/farms/[farmId]/page.tsx new file mode 100644 index 0000000..aa500ce --- /dev/null +++ b/frontend/app/(sidebar)/farms/[farmId]/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { ArrowLeft, MapPin, Plus, Sprout } from "lucide-react"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { AddCropForm } from "./add-crop-form"; +import { CropCard } from "./crop-card"; +import { Farm, Crop } from "@/types"; +import React from "react"; + +const crops: Crop[] = [ + { + id: "crop1", + farmId: "1", + name: "Monthong Durian", + plantedDate: new Date("2023-03-15"), + status: "growing", + }, + { + id: "crop2", + farmId: "1", + name: "Chanee Durian", + plantedDate: new Date("2023-02-20"), + status: "planned", + }, + { + id: "crop3", + farmId: "2", + name: "Kradum Durian", + plantedDate: new Date("2022-11-05"), + status: "harvested", + }, +]; + +const farms: Farm[] = [ + { + id: "1", + name: "Green Valley Farm", + location: "Bangkok", + type: "durian", + createdAt: new Date("2023-01-01"), + }, + { + id: "2", + name: "Golden Farm", + location: "Chiang Mai", + type: "mango", + createdAt: new Date("2022-12-01"), + }, +]; + +const getFarmById = (id: string): Farm | undefined => { + return farms.find((farm) => farm.id === id); +}; + +const getCropsByFarmId = (farmId: string): Crop[] => crops.filter((crop) => crop.farmId === farmId); + +export default function FarmDetailPage({ params }: { params: Promise<{ farmId: string }> }) { + const { farmId } = React.use(params); + + const router = useRouter(); + const [farm] = useState(getFarmById(farmId)); + const [crops, setCrops] = useState(getCropsByFarmId(farmId)); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const handleAddCrop = async (data: Partial) => { + const newCrop: Crop = { + id: Math.random().toString(36).substr(2, 9), + farmId: farm!.id, + name: data.name!, + plantedDate: data.plantedDate!, + status: data.status!, + }; + setCrops((prevCrops) => [...prevCrops, newCrop]); + setIsDialogOpen(false); + }; + + return ( +
+ + +
+ + +
+
+ +
+

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

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

Crops

+ +
+ + setIsDialogOpen(true)}> + +
+
+ +
+
+

Add Crop

+

Plant a new crop

+
+
+
+
+ + + Add New Crop + Fill out the form to add a new crop to your farm. + + setIsDialogOpen(false)} /> + +
+ + {crops.map((crop) => ( + + ))} +
+
+
+
+ ); +} diff --git a/frontend/app/(sidebar)/farms/add-farm-form.tsx b/frontend/app/(sidebar)/farms/add-farm-form.tsx new file mode 100644 index 0000000..288197a --- /dev/null +++ b/frontend/app/(sidebar)/farms/add-farm-form.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +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 type { Farm } from "@/types"; +import { farmFormSchema } from "@/schemas/form.schema"; + +interface AddFarmFormProps { + onSubmit: (data: Partial) => Promise; + onCancel: () => void; +} + +export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) { + const form = useForm>({ + resolver: zodResolver(farmFormSchema), + defaultValues: { + name: "", + location: "", + type: "", + }, + }); + + return ( +
+ + ( + + Farm Name + + + + This is your farm's display name. + + + )} + /> + + ( + + Location + + + + + + )} + /> + + ( + + Farm Type + + + + )} + /> + +
+ + +
+ + + ); +} diff --git a/frontend/app/(sidebar)/farms/farm-card.tsx b/frontend/app/(sidebar)/farms/farm-card.tsx new file mode 100644 index 0000000..210cd1c --- /dev/null +++ b/frontend/app/(sidebar)/farms/farm-card.tsx @@ -0,0 +1,61 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { MapPin, Sprout, Plus } from "lucide-react"; +import type { Farm } from "@/types"; + +export interface FarmCardProps { + variant: "farm" | "add"; + farm?: Farm; + onClick?: () => void; +} + +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"; + + if (variant === "add") { + return ( + + +
+
+ +
+
+

Setup

+

Setup new farm

+
+
+
+
+ ); + } + + if (variant === "farm" && farm) { + return ( + + +
+
+ +
+ {farm.type} +
+
+ +
+
+

{farm.name}

+
+ +

{farm.location}

+
+
+
Created {farm.createdAt.toLocaleDateString()}
+
+
+
+ ); + } + + return null; +} diff --git a/frontend/app/(sidebar)/farms/page.tsx b/frontend/app/(sidebar)/farms/page.tsx new file mode 100644 index 0000000..04895ea --- /dev/null +++ b/frontend/app/(sidebar)/farms/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import { Input } from "@/components/ui/input"; +import { Search } from "lucide-react"; +import { FarmCard } from "./farm-card"; +import { AddFarmForm } from "./add-farm-form"; +import type { Farm } from "@/types"; + +export default function FarmSetupPage() { + 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 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); + }; + + const filteredFarms = farms.filter( + (farm) => + farm.name.toLowerCase().includes(searchQuery.toLowerCase()) || + farm.location.toLowerCase().includes(searchQuery.toLowerCase()) || + farm.type.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( +
+
+

Farms

+
+ + setSearchQuery(e.target.value)} + /> +
+
+ + +
+ + setIsDialogOpen(true)} /> + + + Setup New Farm + Fill out the form to configure your new farm. + + setIsDialogOpen(false)} /> + + + + {filteredFarms.map((farm) => ( + + ))} +
+
+ ); +} diff --git a/frontend/app/setup/layout.tsx b/frontend/app/(sidebar)/layout.tsx similarity index 100% rename from frontend/app/setup/layout.tsx rename to frontend/app/(sidebar)/layout.tsx diff --git a/frontend/app/setup/google-map-with-drawing.tsx b/frontend/app/(sidebar)/setup/google-map-with-drawing.tsx similarity index 76% rename from frontend/app/setup/google-map-with-drawing.tsx rename to frontend/app/(sidebar)/setup/google-map-with-drawing.tsx index 46ddbd5..5362123 100644 --- a/frontend/app/setup/google-map-with-drawing.tsx +++ b/frontend/app/(sidebar)/setup/google-map-with-drawing.tsx @@ -14,24 +14,13 @@ const GoogleMapWithDrawing = () => { const [map, setMap] = useState(null); // Handles drawing complete - const onDrawingComplete = useCallback( - (overlay: google.maps.drawing.OverlayCompleteEvent) => { - console.log("Drawing complete:", overlay); - }, - [] - ); + const onDrawingComplete = useCallback((overlay: google.maps.drawing.OverlayCompleteEvent) => { + console.log("Drawing complete:", overlay); + }, []); return ( - - setMap(map)} - > + + setMap(map)}> {map && ( +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/frontend/schema/authSchema.ts b/frontend/schemas/auth.schema.ts similarity index 100% rename from frontend/schema/authSchema.ts rename to frontend/schemas/auth.schema.ts diff --git a/frontend/schemas/form.schema.ts b/frontend/schemas/form.schema.ts new file mode 100644 index 0000000..387bd96 --- /dev/null +++ b/frontend/schemas/form.schema.ts @@ -0,0 +1,15 @@ +import * as z from "zod"; + +export 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"), +}); + +export const cropFormSchema = z.object({ + name: z.string().min(2, "Crop name must be at least 2 characters"), + plantedDate: z.string().refine((val) => !Number.isNaN(Date.parse(val)), { + message: "Please enter a valid date", + }), + status: z.enum(["growing", "harvested", "planned"]), +}); diff --git a/frontend/types.ts b/frontend/types.ts new file mode 100644 index 0000000..5ae3150 --- /dev/null +++ b/frontend/types.ts @@ -0,0 +1,15 @@ +export interface Crop { + id: string; + farmId: string; + name: string; + plantedDate: Date; + status: "growing" | "harvested" | "planned"; +} + +export interface Farm { + id: string; + name: string; + location: string; + type: string; + createdAt: Date; +}