From aaea2feef7a0967e1a9928600ce9dd424b4164a0 Mon Sep 17 00:00:00 2001 From: Sosokker Date: Sun, 9 Mar 2025 22:09:07 +0700 Subject: [PATCH] ui: add placeholder for marketplace and chatbot --- frontend/app/(sidebar)/chatbot/page.tsx | 693 +++++++++++ .../app/(sidebar)/marketplace/loading.tsx | 99 ++ frontend/app/(sidebar)/marketplace/page.tsx | 1020 +++++++++++++++++ frontend/app/page.tsx | 14 +- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 257 +++++ 6 files changed, 2077 insertions(+), 7 deletions(-) create mode 100644 frontend/app/(sidebar)/chatbot/page.tsx create mode 100644 frontend/app/(sidebar)/marketplace/loading.tsx create mode 100644 frontend/app/(sidebar)/marketplace/page.tsx diff --git a/frontend/app/(sidebar)/chatbot/page.tsx b/frontend/app/(sidebar)/chatbot/page.tsx new file mode 100644 index 0000000..a73b48f --- /dev/null +++ b/frontend/app/(sidebar)/chatbot/page.tsx @@ -0,0 +1,693 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { + ChevronLeft, + Send, + Clock, + X, + Leaf, + MessageSquare, + History, + PanelRightClose, + PanelRightOpen, + Search, + Sparkles, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Avatar } from "@/components/ui/avatar"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import type { Farm, Crop } from "@/types"; + +// Mock data for farms and crops +const mockFarms: Farm[] = [ + { + id: "farm1", + name: "Green Valley Farm", + location: "California", + type: "Organic", + createdAt: new Date("2023-01-15"), + area: "120 acres", + crops: 8, + weather: { + temperature: 24, + humidity: 65, + rainfall: "2mm", + sunlight: 80, + }, + }, + { + id: "farm2", + name: "Sunrise Fields", + location: "Iowa", + type: "Conventional", + createdAt: new Date("2022-11-05"), + area: "350 acres", + crops: 5, + weather: { + temperature: 22, + humidity: 58, + rainfall: "0mm", + sunlight: 90, + }, + }, +]; + +const mockCrops: Crop[] = [ + { + id: "crop1", + farmId: "farm1", + name: "Organic Tomatoes", + plantedDate: new Date("2023-03-10"), + status: "Growing", + variety: "Roma", + area: "15 acres", + healthScore: 92, + progress: 65, + }, + { + id: "crop2", + farmId: "farm1", + name: "Sweet Corn", + plantedDate: new Date("2023-04-05"), + status: "Growing", + variety: "Golden Bantam", + area: "25 acres", + healthScore: 88, + progress: 45, + }, + { + id: "crop3", + farmId: "farm2", + name: "Soybeans", + plantedDate: new Date("2023-05-15"), + status: "Growing", + variety: "Pioneer", + area: "120 acres", + healthScore: 95, + progress: 30, + }, +]; + +// Mock chat history +interface ChatMessage { + id: string; + content: string; + sender: "user" | "bot"; + timestamp: Date; + relatedTo?: { + type: "farm" | "crop"; + id: string; + name: string; + }; +} + +const mockChatHistory: ChatMessage[] = [ + { + id: "msg1", + content: "When should I harvest my tomatoes?", + sender: "user", + timestamp: new Date("2023-07-15T10:30:00"), + relatedTo: { + type: "crop", + id: "crop1", + name: "Organic Tomatoes", + }, + }, + { + id: "msg2", + content: + "Based on the current growth stage of your Roma tomatoes, they should be ready for harvest in approximately 2-3 weeks. The ideal time to harvest is when they've developed their full red color but are still firm to the touch. Keep monitoring the soil moisture levels as consistent watering during the final ripening stage is crucial for flavor development.", + sender: "bot", + timestamp: new Date("2023-07-15T10:30:30"), + }, + { + id: "msg3", + content: "What's the best fertilizer for corn?", + sender: "user", + timestamp: new Date("2023-07-16T14:22:00"), + relatedTo: { + type: "crop", + id: "crop2", + name: "Sweet Corn", + }, + }, + { + id: "msg4", + content: + "For your Sweet Corn at Green Valley Farm, I recommend a nitrogen-rich fertilizer with an NPK ratio of approximately 16-4-8. Corn is a heavy nitrogen feeder, especially during its growth phase. Apply the fertilizer when the plants are knee-high and again when they begin to tassel. Based on your soil analysis, consider supplementing with sulfur to address the slight deficiency detected in your last soil test.", + sender: "bot", + timestamp: new Date("2023-07-16T14:22:45"), + }, +]; + +// Recommended prompts +const recommendedPrompts = [ + { + id: "prompt1", + text: "When should I water my crops?", + category: "Irrigation", + }, + { + id: "prompt2", + text: "How can I improve soil health?", + category: "Soil Management", + }, + { + id: "prompt3", + text: "What pests might affect my crops this season?", + category: "Pest Control", + }, + { + id: "prompt4", + text: "Recommend a crop rotation plan", + category: "Planning", + }, + { + id: "prompt5", + text: "How to maximize yield for my current crops?", + category: "Optimization", + }, + { + id: "prompt6", + text: "What's the best time to harvest?", + category: "Harvesting", + }, +]; + +export default function ChatbotPage() { + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [selectedFarm, setSelectedFarm] = useState(null); + const [selectedCrop, setSelectedCrop] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + const router = useRouter(); + + // Initialize with a welcome message + useEffect(() => { + setMessages([ + { + id: "welcome", + content: + "👋 Hello! I'm ForFarm Assistant, your farming AI companion. How can I help you today? You can ask me about crop management, pest control, weather impacts, or select a specific farm or crop to get tailored advice.", + sender: "bot", + timestamp: new Date(), + }, + ]); + }, []); + + // Scroll to bottom of messages + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // Filter crops based on selected farm + const filteredCrops = selectedFarm ? mockCrops.filter((crop) => crop.farmId === selectedFarm) : mockCrops; + + // Handle sending a message + const handleSendMessage = (content: string = inputValue) => { + if (!content.trim()) return; + + // Create user message + const userMessage: ChatMessage = { + id: `user-${Date.now()}`, + content, + sender: "user", + timestamp: new Date(), + ...(selectedFarm || selectedCrop + ? { + relatedTo: { + type: selectedCrop ? "crop" : "farm", + id: selectedCrop || selectedFarm || "", + name: selectedCrop + ? mockCrops.find((c) => c.id === selectedCrop)?.name || "" + : mockFarms.find((f) => f.id === selectedFarm)?.name || "", + }, + } + : {}), + }; + + setMessages((prev) => [...prev, userMessage]); + setInputValue(""); + setIsLoading(true); + + // Simulate bot response after a delay + setTimeout(() => { + const botResponse: ChatMessage = { + id: `bot-${Date.now()}`, + content: generateBotResponse(content, selectedFarm, selectedCrop), + sender: "bot", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, botResponse]); + setIsLoading(false); + }, 1500); + }; + + // Generate a bot response based on the user's message and selected farm/crop + const generateBotResponse = (message: string, farmId: string | null, cropId: string | null): string => { + const lowerMessage = message.toLowerCase(); + + // Get farm and crop details if selected + const farm = farmId ? mockFarms.find((f) => f.id === farmId) : null; + const crop = cropId ? mockCrops.find((c) => c.id === cropId) : null; + + // Personalize response based on selected farm/crop + let contextPrefix = ""; + if (crop) { + contextPrefix = `For your ${crop.name} (${crop.variety}) at ${farm?.name || "your farm"}, `; + } else if (farm) { + contextPrefix = `For ${farm.name}, `; + } + + // Generate response based on message content + if (lowerMessage.includes("water") || lowerMessage.includes("irrigation")) { + return `${contextPrefix}I recommend watering deeply but infrequently to encourage strong root growth. Based on the current weather conditions${ + farm ? ` in ${farm.location}` : "" + } (${farm?.weather?.rainfall || "minimal"} rainfall recently), you should water ${ + crop ? `your ${crop.name}` : "your crops" + } approximately 2-3 times per week, ensuring the soil remains moist but not waterlogged.`; + } else if (lowerMessage.includes("fertiliz") || lowerMessage.includes("nutrient")) { + return `${contextPrefix}a balanced NPK fertilizer with a ratio of 10-10-10 would be suitable for general application. ${ + crop + ? `For ${crop.name} specifically, consider increasing ${ + crop.name.toLowerCase().includes("tomato") + ? "potassium" + : crop.name.toLowerCase().includes("corn") + ? "nitrogen" + : "phosphorus" + } for optimal growth during the current ${ + crop.progress && crop.progress < 30 ? "early" : crop.progress && crop.progress < 70 ? "middle" : "late" + } growth stage.` + : "" + }`; + } else if (lowerMessage.includes("pest") || lowerMessage.includes("insect") || lowerMessage.includes("disease")) { + return `${contextPrefix}monitor for ${ + crop + ? crop.name.toLowerCase().includes("tomato") + ? "tomato hornworms, aphids, and early blight" + : crop.name.toLowerCase().includes("corn") + ? "corn borers, rootworms, and rust" + : "common agricultural pests" + : "common agricultural pests like aphids, beetles, and fungal diseases" + }. I recommend implementing integrated pest management (IPM) practices, including regular scouting, beneficial insects, and targeted treatments only when necessary.`; + } else if (lowerMessage.includes("harvest") || lowerMessage.includes("yield")) { + return `${contextPrefix}${ + crop + ? `your ${crop.name} should be ready to harvest in approximately ${Math.max( + 1, + Math.round((100 - (crop.progress || 50)) / 10) + )} weeks based on the current growth stage. Look for ${ + crop.name.toLowerCase().includes("tomato") + ? "firm, fully colored fruits" + : crop.name.toLowerCase().includes("corn") + ? "full ears with dried silk and plump kernels" + : "signs of maturity specific to this crop type" + }` + : "harvest timing depends on the specific crops you're growing, but generally you should look for visual cues of ripeness and maturity" + }.`; + } else if (lowerMessage.includes("soil") || lowerMessage.includes("compost")) { + return `${contextPrefix}improving soil health is crucial for sustainable farming. I recommend regular soil testing, adding organic matter through compost or cover crops, practicing crop rotation, and minimizing soil disturbance. ${ + farm + ? `Based on the soil type common in ${farm.location}, you might also consider adding ${ + farm.location.includes("California") ? "gypsum to improve drainage" : "lime to adjust pH levels" + }.` + : "" + }`; + } else if (lowerMessage.includes("weather") || lowerMessage.includes("forecast") || lowerMessage.includes("rain")) { + return `${contextPrefix}${ + farm + ? `the current conditions show temperature at ${farm.weather?.temperature}°C with ${farm.weather?.humidity}% humidity. There's been ${farm.weather?.rainfall} of rainfall recently, and sunlight levels are at ${farm.weather?.sunlight}% of optimal.` + : "I recommend checking your local agricultural weather service for the most accurate forecast for your specific location." + } ${ + crop + ? `For your ${crop.name}, ${ + farm?.weather?.rainfall === "0mm" + ? "the dry conditions mean you should increase irrigation" + : "the recent rainfall means you can reduce irrigation temporarily" + }.` + : "" + }`; + } else { + return `${contextPrefix}I understand you're asking about "${message}". To provide the most helpful advice, could you provide more specific details about your farming goals or challenges? I'm here to help with crop management, pest control, irrigation strategies, and more.`; + } + }; + + // Handle selecting a farm + const handleFarmSelect = (farmId: string) => { + setSelectedFarm(farmId); + setSelectedCrop(null); // Reset crop selection when farm changes + }; + + // Handle selecting a crop + const handleCropSelect = (cropId: string) => { + setSelectedCrop(cropId); + }; + + // Handle clicking a recommended prompt + const handlePromptClick = (promptText: string) => { + setInputValue(promptText); + handleSendMessage(promptText); + }; + + // Handle loading a chat history item + const handleLoadChatHistory = (messageId: string) => { + // Find the message in history + const historyItem = mockChatHistory.find((msg) => msg.id === messageId); + if (!historyItem) return; + + // Set related farm/crop if available + if (historyItem.relatedTo) { + if (historyItem.relatedTo.type === "farm") { + setSelectedFarm(historyItem.relatedTo.id); + setSelectedCrop(null); + } else if (historyItem.relatedTo.type === "crop") { + const crop = mockCrops.find((c) => c.id === historyItem.relatedTo?.id); + if (crop) { + setSelectedFarm(crop.farmId); + setSelectedCrop(historyItem.relatedTo.id); + } + } + } + + // Load the conversation + const conversation = mockChatHistory.filter( + (msg) => + msg.id === messageId || + (msg.timestamp >= historyItem.timestamp && msg.timestamp <= new Date(historyItem.timestamp.getTime() + 60000)) + ); + + setMessages(conversation); + setIsHistoryOpen(false); + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+ +

ForFarm Assistant

+
+
+ +
+
+ + {/* Main content */} +
+ {/* Chat area */} +
+ {/* Farm/Crop selector */} +
+
+
+ + +
+
+ + +
+
+
+ + {/* Messages */} + +
+ {messages.map((message) => ( +
+
+ {message.relatedTo && ( +
+ + {message.relatedTo.type === "farm" ? "🏡 " : "🌱 "} + {message.relatedTo.name} + +
+ )} +
{message.content}
+
+ {message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} +
+
+
+ ))} + {isLoading && ( +
+
+
+
+
+
+ + ForFarm Assistant is typing... + +
+
+
+ )} +
+
+ + + {/* Recommended prompts */} +
+

+ + Recommended Questions +

+
+ {recommendedPrompts.map((prompt) => ( + + ))} +
+
+ + {/* Input area */} +
+
+ setInputValue(e.target.value)} + placeholder="Ask about your farm or crops..." + className="flex-1" + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }} + /> + +
+
+
+ + {/* Chat history sidebar */} + {isHistoryOpen && ( +
+
+

+ + Chat History +

+ +
+ +
+
+ + +
+
+ + + + + Recent + + + By Farm + + + By Crop + + + + + +
+ {mockChatHistory + .filter((msg) => msg.sender === "user") + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .map((message) => ( + handleLoadChatHistory(message.id)}> +
+ +
+ {message.relatedTo?.name.substring(0, 2) || "Me"} +
+
+
+
+

+ {message.relatedTo ? message.relatedTo.name : "General Question"} +

+

+ + {message.timestamp.toLocaleDateString()} +

+
+

+ {message.content} +

+
+
+
+ ))} +
+
+ + +
+ {mockFarms.map((farm) => ( +
+

{farm.name}

+
+ {mockChatHistory + .filter( + (msg) => + msg.sender === "user" && msg.relatedTo?.type === "farm" && msg.relatedTo.id === farm.id + ) + .map((message) => ( + handleLoadChatHistory(message.id)}> +

{message.content}

+

+ {message.timestamp.toLocaleDateString()} +

+
+ ))} +
+ +
+ ))} +
+
+ + +
+ {mockCrops.map((crop) => ( +
+

+ + {crop.name} + + ({mockFarms.find((f) => f.id === crop.farmId)?.name}) + +

+
+ {mockChatHistory + .filter( + (msg) => + msg.sender === "user" && msg.relatedTo?.type === "crop" && msg.relatedTo.id === crop.id + ) + .map((message) => ( + handleLoadChatHistory(message.id)}> +

{message.content}

+

+ {message.timestamp.toLocaleDateString()} +

+
+ ))} +
+ +
+ ))} +
+
+
+
+
+ )} +
+
+ ); +} diff --git a/frontend/app/(sidebar)/marketplace/loading.tsx b/frontend/app/(sidebar)/marketplace/loading.tsx new file mode 100644 index 0000000..0b57d1f --- /dev/null +++ b/frontend/app/(sidebar)/marketplace/loading.tsx @@ -0,0 +1,99 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { RefreshCw } from "lucide-react"; + +export default function MarketplaceLoading() { + return ( +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +

Loading market data...

+
+
+ +
+ + + +
+
+
+ +
+ + + + + + +
+ + + + + +
+
+
+ + + + + + + +
+ + + +
+
+
+
+
+ + + + + + + +
+ + + + +
+
+
+
+ ); +} diff --git a/frontend/app/(sidebar)/marketplace/page.tsx b/frontend/app/(sidebar)/marketplace/page.tsx new file mode 100644 index 0000000..33b21f0 --- /dev/null +++ b/frontend/app/(sidebar)/marketplace/page.tsx @@ -0,0 +1,1020 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@/components/ui/card"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger +} from "@/components/ui/tabs"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + BarChart, + Bar, + Cell +} from "recharts"; +import { + ArrowUpRight, + ArrowDownRight, + TrendingUp, + Calendar, + MapPin, + RefreshCw, + AlertCircle, + ChevronRight, + Leaf, + BarChart3, + LineChartIcon, + PieChart +} from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Progress } from "@/components/ui/progress"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { + Alert, + AlertDescription, + AlertTitle +} from "@/components/ui/alert"; + +// Define types for market data + +interface ITrend { + direction: "up" | "down"; + value: string; +} + +interface IMarketPrice { + market: string; + price: number; + demand: number; + trend: ITrend; +} + +export interface IMarketData { + id: string; + name: string; + marketPrices: IMarketPrice[]; + averagePrice: number; + recommendedPrice: number; + demandScore: number; + opportunity: boolean; +} + +export interface IHistoricalData { + date: string; + price: number; + volume: number; +} + +export interface IMarketComparison { + name: string; + price: number; + demand: number; +} + +// Types for tooltip props from recharts +interface CustomTooltipProps { + active?: boolean; + payload?: { value: number }[]; + label?: string; +} + +// Types for MarketOpportunity props +interface MarketOpportunityProps { + crop: string; + data: IMarketComparison[]; +} + +// Mock data for market prices +const generateMarketData = (): IMarketData[] => { + const crops = [ + "Corn", + "Wheat", + "Soybeans", + "Rice", + "Potatoes", + "Tomatoes", + "Apples", + "Oranges" + ]; + const markets = [ + "National Market", + "Regional Hub", + "Local Market", + "Export Market", + "Wholesale Market" + ]; + + const getRandomPrice = (base: number) => + Number((base + Math.random() * 2).toFixed(2)); + const getRandomDemand = () => Math.floor(Math.random() * 100); + const getRandomTrend = (): ITrend => + Math.random() > 0.5 + ? { direction: "up", value: (Math.random() * 5).toFixed(1) } + : { direction: "down", value: (Math.random() * 5).toFixed(1) }; + + return crops.map((crop) => { + const basePrice = + crop === "Corn" + ? 4 + : crop === "Wheat" + ? 6 + : crop === "Soybeans" + ? 10 + : crop === "Rice" + ? 12 + : crop === "Potatoes" + ? 3 + : crop === "Tomatoes" + ? 2 + : crop === "Apples" + ? 1.5 + : 8; + + return { + id: crypto.randomUUID(), + name: crop, + marketPrices: markets.map((market) => ({ + market, + price: getRandomPrice(basePrice), + demand: getRandomDemand(), + trend: getRandomTrend() + })), + averagePrice: getRandomPrice(basePrice - 0.5), + recommendedPrice: getRandomPrice(basePrice + 0.2), + demandScore: getRandomDemand(), + opportunity: Math.random() > 0.7 + }; + }); +}; + +// Generate historical price data for a specific crop +const generateHistoricalData = ( + crop: string, + days = 30 +): IHistoricalData[] => { + const basePrice = + crop === "Corn" + ? 4 + : crop === "Wheat" + ? 6 + : crop === "Soybeans" + ? 10 + : crop === "Rice" + ? 12 + : crop === "Potatoes" + ? 3 + : crop === "Tomatoes" + ? 2 + : crop === "Apples" + ? 1.5 + : 8; + + const data: IHistoricalData[] = []; + let currentPrice = basePrice; + + for (let i = days; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + const change = (Math.random() - 0.5) * 0.4; + currentPrice = Math.max(0.5, currentPrice + change); + + data.push({ + date: date.toISOString().split("T")[0], + price: Number(currentPrice.toFixed(2)), + volume: Math.floor(Math.random() * 1000) + 200 + }); + } + + return data; +}; + +// Generate market comparison data +const generateMarketComparisonData = ( + crop: string +): IMarketComparison[] => { + const markets = [ + "National Market", + "Regional Hub", + "Local Market", + "Export Market", + "Wholesale Market" + ]; + const basePrice = + crop === "Corn" + ? 4 + : crop === "Wheat" + ? 6 + : crop === "Soybeans" + ? 10 + : crop === "Rice" + ? 12 + : crop === "Potatoes" + ? 3 + : crop === "Tomatoes" + ? 2 + : crop === "Apples" + ? 1.5 + : 8; + + return markets.map((market) => ({ + name: market, + price: Number((basePrice + (Math.random() - 0.5) * 2).toFixed(2)), + demand: Math.floor(Math.random() * 100) + })); +}; + +// Custom tooltip for the price chart +const CustomTooltip = ({ + active, + payload, + label +}: CustomTooltipProps) => { + if (active && payload && payload.length) { + return ( + +

{label}

+

Price: ${payload[0].value}

+ {payload[1] && ( +

+ Volume: {payload[1].value} units +

+ )} +
+ ); + } + return null; +}; + +// Market opportunity component +const MarketOpportunity = ({ crop, data }: MarketOpportunityProps) => { + const highestPrice = Math.max(...data.map((item) => item.price)); + const bestMarket = data.find((item) => item.price === highestPrice); + const highestDemand = Math.max(...data.map((item) => item.demand)); + const highDemandMarket = data.find( + (item) => item.demand === highestDemand + ); + + return ( + + + + + Sales Opportunity for {crop} + + + Based on current market conditions + + + +
+
+

+ Best Price Opportunity +

+
+
+

+ ${bestMarket?.price} +

+

+ {bestMarket?.name} +

+
+ + {Math.round((bestMarket!.price / (highestPrice - 1)) * 100)}% + above average + +
+
+ + + +
+

+ Highest Demand +

+
+
+

+ {highDemandMarket?.name} +

+
+ + + {highDemandMarket?.demand}% demand + +
+
+ +
+
+ + + + + Recommendation + + + Consider selling your {crop} at {bestMarket?.name} within + the next 7 days to maximize profit. + + +
+
+
+ ); +}; + +export default function MarketplacePage() { + const searchParams = useSearchParams(); + const initialCrop = searchParams.get("crop") || "Corn"; + + const [selectedCrop, setSelectedCrop] = useState(initialCrop); + const [timeRange, setTimeRange] = useState("30"); + const [isLoading, setIsLoading] = useState(true); + const [marketData, setMarketData] = useState([]); + const [historicalData, setHistoricalData] = useState([]); + const [marketComparison, setMarketComparison] = + useState([]); + const [lastUpdated, setLastUpdated] = useState(new Date()); + + useEffect(() => { + setIsLoading(true); + const timer = setTimeout(() => { + setMarketData(generateMarketData()); + setHistoricalData( + generateHistoricalData(selectedCrop, Number.parseInt(timeRange)) + ); + setMarketComparison(generateMarketComparisonData(selectedCrop)); + setLastUpdated(new Date()); + setIsLoading(false); + }, 1200); + + return () => clearTimeout(timer); + }, [selectedCrop, timeRange]); + + const handleRefresh = () => { + setIsLoading(true); + setTimeout(() => { + setMarketData(generateMarketData()); + setHistoricalData( + generateHistoricalData(selectedCrop, Number.parseInt(timeRange)) + ); + setMarketComparison(generateMarketComparisonData(selectedCrop)); + setLastUpdated(new Date()); + setIsLoading(false); + }, 1000); + }; + + // Removed unused variable "selectedCropData" + + const getTrendColor = (trend: ITrend) => { + return trend.direction === "up" ? "text-green-600" : "text-red-600"; + }; + + const getTrendIcon = (trend: ITrend) => { + return trend.direction === "up" ? ( + + ) : ( + + ); + }; + + return ( +
+
+
+

+ Marketplace Information +

+

+ Make informed decisions with real-time market data and price + analytics +

+
+ +
+ +
+ Last updated: {lastUpdated.toLocaleTimeString()} +
+
+
+ +
+ + +
+
+ Price Analytics + + Track price trends and market movements + +
+
+ + + +
+
+
+ + {isLoading ? ( +
+
+ +

+ Loading market data... +

+
+
+ ) : ( + + + + Price Trend + + + Market Comparison + + + Demand Analysis + + + + +
+ + + + { + const date = new Date(value); + return `${date.getMonth() + 1}/${ + date.getDate() + }`; + }} + /> + `$${value}`} + domain={["dataMin - 0.5", "dataMax + 0.5"]} + /> + + } /> + + + + + +
+ +
+ + +
+
+

+ Current Price +

+

+ $ + {historicalData[ + historicalData.length - 1 + ]?.price.toFixed(2)} +

+
+
+ +
+
+
+
+ + + +
+
+

+ 30-Day Average +

+

+ $ + {( + historicalData.reduce( + (sum, item) => sum + item.price, + 0 + ) / historicalData.length + ).toFixed(2)} +

+
+
+ +
+
+
+
+ + + +
+
+

+ Recommended Price +

+

+ $ + {( + historicalData[historicalData.length - 1] + ?.price * 1.05 + ).toFixed(2)} +

+
+ + +5% margin + +
+
+
+
+
+ + +
+ + + + + `$${value}`} + /> + + + + {marketComparison.map((entry, index) => ( + item.price + ) + ) + ? "#15803d" + : "#16a34a" + } + /> + ))} + + + +
+ +
+ + + Market comparison for {selectedCrop} as of{" "} + {new Date().toLocaleDateString()} + + + + Market + Price + Demand + Price Difference + + Action + + + + + {marketComparison.map((market) => { + const avgPrice = + marketComparison.reduce( + (sum, m) => sum + m.price, + 0 + ) / marketComparison.length; + const priceDiff = ( + ((market.price - avgPrice) / avgPrice) * + 100 + ).toFixed(1); + const isPriceHigh = Number.parseFloat(priceDiff) > 0; + + return ( + + setSelectedCrop(market.name) + } + > + + {market.name} + + + ${market.price.toFixed(2)} + + +
+ + {market.demand}% +
+
+ +
+ {isPriceHigh ? ( + + ) : ( + + )} + {priceDiff}% +
+
+ + + +
+ ); + })} +
+
+
+
+ + +
+ + + + Demand Forecast + + + Projected demand for {selectedCrop} over the next 30 days + + + +
+ {marketComparison.map((market) => ( +
+
+ {market.name} + + {market.demand}% + +
+ +
+ ))} +
+
+
+ + +
+
+
+ )} +
+
+ +
+ + + Market Summary + + Today's market overview + + + + {isLoading ? ( +
+ + + +
+ ) : ( + +
+ {marketData.slice(0, 5).map((crop) => ( +
+
+

{crop.name}

+
+ ${crop.averagePrice.toFixed(2)} + {crop.marketPrices[0].trend.direction === "up" ? ( + + + {crop.marketPrices[0].trend.value}% + + ) : ( + + + {crop.marketPrices[0].trend.value}% + + )} +
+
+ +
+ ))} +
+
+ )} +
+
+ + + + Top Opportunities + + Best selling opportunities today + + + + {isLoading ? ( +
+ + + +
+ ) : ( +
+ {marketData + .filter((crop) => crop.opportunity) + .slice(0, 3) + .map((crop) => { + const bestMarket = crop.marketPrices.reduce( + (best, current) => + current.price > best.price ? current : best, + crop.marketPrices[0] + ); + return ( +
+
+
+

{crop.name}

+

+ {bestMarket.market} - $ + {bestMarket.price.toFixed(2)} +

+
+ + High Demand + +
+
+
+ + + Recommended + +
+ +
+
+ ); + })} +
+ )} +
+
+
+
+ + + + Market Price Table + + Comprehensive price data across all markets and crops + + + + {isLoading ? ( +
+ + + + +
+ ) : ( +
+ + + + Crop + {marketData[0]?.marketPrices.map((market) => ( + + {market.market} + + ))} + Average + Recommended + + + + {marketData.map((crop) => ( + setSelectedCrop(crop.name)} + > + + {crop.name} + + {crop.marketPrices.map((market) => ( + +
+ ${market.price.toFixed(2)} + + {getTrendIcon(market.trend)} + +
+
+ ))} + + ${crop.averagePrice.toFixed(2)} + + + ${crop.recommendedPrice.toFixed(2)} + +
+ ))} +
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index de17932..86c3b56 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -39,22 +39,22 @@ export default function Home() {
Log in diff --git a/frontend/package.json b/frontend/package.json index 5be4672..2bc983b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "react-day-picker": "8.10.1", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", + "recharts": "^2.15.1", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.2" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3982219..202a360 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) + recharts: + specifier: ^2.15.1 + version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tailwind-merge: specifier: ^3.0.1 version: 3.0.1 @@ -953,6 +956,33 @@ packages: peerDependencies: react: ^18 || ^19 + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1233,6 +1263,50 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -1268,6 +1342,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1300,6 +1377,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1469,9 +1549,16 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.2.2: + resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -1650,6 +1737,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -1848,6 +1939,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2167,6 +2261,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2187,6 +2284,12 @@ packages: '@types/react': optional: true + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -2197,6 +2300,12 @@ packages: '@types/react': optional: true + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -2208,6 +2317,16 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.1: + resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2424,6 +2543,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2505,6 +2627,9 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3323,6 +3448,30 @@ snapshots: '@tanstack/query-core': 5.66.0 react: 19.0.0 + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/estree@1.0.6': {} '@types/google.maps@3.58.1': {} @@ -3653,6 +3802,44 @@ snapshots: csstype@3.1.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -3683,6 +3870,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3712,6 +3901,11 @@ snapshots: dependencies: esutils: 2.0.3 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.26.7 + csstype: 3.1.3 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.1 @@ -4026,8 +4220,12 @@ snapshots: esutils@2.0.3: {} + eventemitter3@4.0.7: {} + fast-deep-equal@3.1.3: {} + fast-equals@5.2.2: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4211,6 +4409,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -4412,6 +4612,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.21: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -4711,6 +4913,8 @@ snapshots: react-is@16.13.1: {} + react-is@18.3.1: {} + react-remove-scroll-bar@2.3.8(@types/react@19.0.8)(react@19.0.0): dependencies: react: 19.0.0 @@ -4730,6 +4934,14 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + react-smooth@4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + fast-equals: 5.2.2 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-style-singleton@2.2.3(@types/react@19.0.8)(react@19.0.0): dependencies: get-nonce: 1.0.1 @@ -4738,6 +4950,15 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react@19.0.0: {} read-cache@1.0.0: @@ -4748,6 +4969,23 @@ snapshots: dependencies: picomatch: 2.3.1 + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -5055,6 +5293,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-invariant@1.3.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5145,6 +5385,23 @@ snapshots: uuid@8.3.2: {} + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0