diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx new file mode 100644 index 0000000..7beeff7 --- /dev/null +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/analytics-dialog.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { LineChart, Sprout, Droplets, Sun } from "lucide-react"; +import type { Crop, CropAnalytics } from "@/types"; + +interface AnalyticsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + crop: Crop; + analytics: CropAnalytics; +} + +export function AnalyticsDialog({ open, onOpenChange, crop, analytics }: AnalyticsDialogProps) { + return ( + + + + Crop Analytics - {crop.name} + + + + + Overview + Growth + Environment + + + +
+ + + Growth Rate + + + +
+2.5%
+

+20.1% from last week

+
+
+ + + Water Usage + + + +
15.2L
+

per day average

+
+
+ + + Sunlight + + + +
{analytics.sunlight}%
+

optimal conditions

+
+
+
+ + + + Growth Timeline + Daily growth rate over time + + + + Growth chart placeholder + + +
+ + + + + Detailed Growth Analysis + Comprehensive growth metrics + + + Detailed growth analysis placeholder + + + + + + + + Environmental Conditions + Temperature, humidity, and more + + + Environmental metrics placeholder + + + +
+
+
+ ); +} diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx new file mode 100644 index 0000000..e704521 --- /dev/null +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/chatbot-dialog.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useState } from "react"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Send } from "lucide-react"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; + +interface Message { + role: "user" | "assistant"; + content: string; +} + +interface ChatbotDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + cropName: string; +} + +export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogProps) { + const [messages, setMessages] = useState([ + { + role: "assistant", + content: `Hello! I'm your farming assistant. How can I help you with your ${cropName} today?`, + }, + ]); + const [input, setInput] = useState(""); + + const handleSend = () => { + if (!input.trim()) return; + + const newMessages: Message[] = [ + ...messages, + { role: "user", content: input }, + { role: "assistant", content: `Here's some information about ${cropName}: [AI response placeholder]` }, + ]; + setMessages(newMessages); + setInput(""); + }; + + return ( + + + + + +
+
+

Farming Assistant

+

Ask questions about your {cropName}

+
+ + +
+ {messages.map((message, i) => ( +
+
+ {message.content} +
+
+ ))} +
+
+ +
+
{ + e.preventDefault(); + handleSend(); + }} + className="flex gap-2"> + setInput(e.target.value)} /> + +
+
+
+
+
+ ); +} diff --git a/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx new file mode 100644 index 0000000..7e94118 --- /dev/null +++ b/frontend/app/(sidebar)/farms/[farmId]/crops/[cropId]/page.tsx @@ -0,0 +1,253 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { + ArrowLeft, + MapPin, + Sprout, + LineChart, + MessageSquare, + Settings, + AlertCircle, + Droplets, + Sun, + ThermometerSun, + Timer, + ListCollapse, +} from "lucide-react"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ChatbotDialog } from "./chatbot-dialog"; +import { AnalyticsDialog } from "./analytics-dialog"; +import type { Crop, CropAnalytics } from "@/types"; + +const getCropById = (id: string): Crop => { + return { + id, + farmId: "1", + name: "Monthong Durian", + plantedDate: new Date("2024-01-15"), + status: "growing", + }; +}; + +const getAnalyticsByCropId = (id: string): CropAnalytics => { + return { + cropId: id, + growthProgress: 45, // Percentage + humidity: 75, // Percentage + temperature: 28, // °C + sunlight: 85, // Percentage + waterLevel: 65, // Percentage + plantHealth: "good", // "good", "warning", "critical" + nextAction: "Water the plant", + nextActionDue: new Date("2024-02-15"), + }; +}; + +export default function CropDetailPage({ params }: { params: Promise<{ farmId: string; cropId: string }> }) { + const { farmId, cropId } = React.use(params); + + const router = useRouter(); + const [crop] = useState(getCropById(cropId)); + const analytics = getAnalyticsByCropId(cropId); + + // Colors for plant health badge. + const healthColors = { + good: "text-green-500", + warning: "text-yellow-500", + critical: "text-red-500", + }; + + const actions = [ + { + title: "Analytics", + icon: LineChart, + description: "View detailed growth analytics", + onClick: () => setIsAnalyticsOpen(true), + }, + { + title: "Chat Assistant", + icon: MessageSquare, + description: "Get help and advice", + onClick: () => setIsChatOpen(true), + }, + { + title: "Detailed", + icon: ListCollapse, + description: "View detailed of crop", + onClick: () => console.log("Detailed clicked"), + }, + { + title: "Settings", + icon: Settings, + description: "Configure crop settings", + onClick: () => console.log("Settings clicked"), + }, + { + title: "Report Issue", + icon: AlertCircle, + description: "Report a problem", + onClick: () => console.log("Report clicked"), + }, + ]; + + const [isChatOpen, setIsChatOpen] = useState(false); + const [isAnalyticsOpen, setIsAnalyticsOpen] = useState(false); + + return ( +
+ + +
+ {/* Left Column - Crop Details */} +
+ + +
+
+ +
+
+

{crop.name}

+

Planted on {crop.plantedDate.toLocaleDateString()}

+
+
+ + {analytics.plantHealth.toUpperCase()} + +
+ +
+
+

Growth Progress

+ +

{analytics.growthProgress}% Complete

+
+ +
+
+
+ + Humidity +
+

{analytics.humidity}%

+
+
+
+ + Temperature +
+

{analytics.temperature}°C

+
+
+
+ + Sunlight +
+

{analytics.sunlight}%

+
+
+
+ + Water Level +
+

{analytics.waterLevel}%

+
+
+ + + +
+
+ + Next Action Required +
+
+

{analytics.nextAction}

+

+ Due by {analytics.nextActionDue.toLocaleDateString()} +

+
+
+
+
+
+ + + +

Actions

+
+ +
+ {actions.map((action) => ( + + ))} +
+
+
+
+ +
+ + +
+
+ +

+ Map placeholder +
+ Click to view full map +

+
+
+
+
+ + + +

Quick Analytics

+
+ + + + Growth + Health + Water + + + Growth chart placeholder + + + Health metrics placeholder + + + Water usage placeholder + + + +
+
+
+ + + + +
+ ); +} diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/frontend/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx new file mode 100644 index 0000000..4fc3b47 --- /dev/null +++ b/frontend/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/frontend/components/ui/scroll-area.tsx b/frontend/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0b4a48d --- /dev/null +++ b/frontend/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/frontend/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/frontend/package.json b/frontend/package.json index e7de5e8..eca4aa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-visually-hidden": "^1.1.2", "@react-google-maps/api": "^2.20.6", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.66.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 271ea63..1f662f2 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-visually-hidden': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@react-google-maps/api': specifier: ^2.20.6 version: 2.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) diff --git a/frontend/types.ts b/frontend/types.ts index 5ae3150..98fc9b1 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -6,6 +6,18 @@ export interface Crop { status: "growing" | "harvested" | "planned"; } +export interface CropAnalytics { + cropId: string; + humidity: number; + temperature: number; + sunlight: number; + waterLevel: number; + growthProgress: number; + plantHealth: "good" | "warning" | "critical"; + nextAction: string; + nextActionDue: Date; +} + export interface Farm { id: string; name: string;