Merge branch 'feature-farm-setup' of https://github.com/ForFarmTeam/ForFarm into feature-farm-setup

This commit is contained in:
Sosokker 2025-03-27 23:44:34 +07:00
commit a5108e9990
20 changed files with 11228 additions and 4048 deletions

View File

@ -7,6 +7,7 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s
import DynamicBreadcrumb from "./dynamic-breadcrumb"; import DynamicBreadcrumb from "./dynamic-breadcrumb";
import { extractRoute } from "@/lib/utils"; import { extractRoute } from "@/lib/utils";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Toaster } from "@/components/ui/sonner";
export default function AppLayout({ export default function AppLayout({
children, children,
@ -29,6 +30,7 @@ export default function AppLayout({
</div> </div>
</header> </header>
{children} {children}
<Toaster />
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>
); );

View File

@ -0,0 +1,22 @@
"use client";
import { Card } from "@/components/ui/card";
interface CustomTooltipProps {
active?: boolean;
payload?: { value: number }[];
label?: string;
}
export default function CustomTooltip({ active, payload, label }: CustomTooltipProps) {
if (active && payload && payload.length) {
return (
<Card className="bg-white dark:bg-gray-800 p-2 shadow-md border-none">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-primary">Price: ${payload[0].value}</p>
{payload[1] && <p className="text-sm text-gray-500">Volume: {payload[1].value} units</p>}
</Card>
);
}
return null;
}

View File

@ -0,0 +1,38 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import MarketOpportunity from "./MarketOpportunity";
import { IMarketComparison } from "@/lib/marketData";
interface DemandAnalysisProps {
selectedCrop: string;
marketComparison: IMarketComparison[];
}
export default function DemandAnalysis({ selectedCrop, marketComparison }: DemandAnalysisProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Demand Forecast</CardTitle>
<CardDescription>Projected demand for {selectedCrop} over the next 30 days</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{marketComparison.map((market) => (
<div key={market.name} className="space-y-1">
<div className="flex justify-between text-sm">
<span>{market.name}</span>
<span className="font-medium">{market.demand}%</span>
</div>
<Progress value={market.demand} className="h-2" />
</div>
))}
</div>
</CardContent>
</Card>
<MarketOpportunity crop={selectedCrop} data={marketComparison} />
</div>
);
}

View File

@ -0,0 +1,95 @@
"use client";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from "recharts";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { ArrowUpRight, ArrowDownRight, ChevronRight } from "lucide-react";
import { Progress } from "@/components/ui/progress";
import { IMarketComparison } from "@/lib/marketData";
interface MarketComparisonTabProps {
marketComparison: IMarketComparison[];
selectedCrop: string;
onSelectCrop: (crop: string) => void;
}
export default function MarketComparisonTab({
marketComparison,
selectedCrop,
onSelectCrop,
}: MarketComparisonTabProps) {
return (
<>
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={marketComparison} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="name" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} tickFormatter={(value) => `$${value}`} />
<Tooltip />
<Legend />
<Bar dataKey="price" name="Price per unit" fill="#16a34a" radius={[4, 4, 0, 0]}>
{marketComparison.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.price === Math.max(...marketComparison.map((item) => item.price)) ? "#15803d" : "#16a34a"}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-4">
<Table>
<TableCaption>
Market comparison for {selectedCrop} as of {new Date().toLocaleDateString()}
</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Market</TableHead>
<TableHead>Price</TableHead>
<TableHead>Demand</TableHead>
<TableHead>Price Difference</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{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 (
<TableRow
key={market.name}
className="cursor-pointer hover:bg-muted/50"
onClick={() => onSelectCrop(market.name)}>
<TableCell className="font-medium">{market.name}</TableCell>
<TableCell>${market.price.toFixed(2)}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={market.demand} className="h-2 w-16" />
<span>{market.demand}%</span>
</div>
</TableCell>
<TableCell className={isPriceHigh ? "text-green-600" : "text-red-600"}>
<div className="flex items-center gap-1">
{isPriceHigh ? <ArrowUpRight className="h-4 w-4" /> : <ArrowDownRight className="h-4 w-4" />}
{priceDiff}%
</div>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" className="h-8 gap-1">
Details <ChevronRight className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</>
);
}

View File

@ -0,0 +1,76 @@
"use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { ArrowUpRight, ArrowDownRight, TrendingUp, MapPin, AlertCircle } from "lucide-react";
import { IMarketComparison } from "@/lib/marketData";
interface MarketOpportunityProps {
crop: string;
data: IMarketComparison[];
}
export default function 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 (
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center">
<TrendingUp className="h-5 w-5 mr-2 text-green-500" />
Sales Opportunity for {crop}
</CardTitle>
<CardDescription>Based on current market conditions</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<p className="text-sm font-medium mb-1">Best Price Opportunity</p>
<div className="flex items-center justify-between">
<div>
<p className="text-lg font-bold text-green-600">${bestMarket?.price}</p>
<p className="text-sm text-muted-foreground">{bestMarket?.name}</p>
</div>
<Badge className="bg-green-100 text-green-800 hover:bg-green-200">
{Math.round((bestMarket!.price / (highestPrice - 1)) * 100)}% above average
</Badge>
</div>
</div>
<Separator />
<div>
<p className="text-sm font-medium mb-1">Highest Demand</p>
<div className="flex items-center justify-between">
<div>
<p className="text-lg font-bold">{highDemandMarket?.name}</p>
<div className="flex items-center">
<Progress value={highDemandMarket?.demand} className="h-2 w-24 mr-2" />
<span className="text-sm">{highDemandMarket?.demand}% demand</span>
</div>
</div>
<Button variant="outline" size="sm" className="gap-1">
<MapPin className="h-4 w-4" /> View Market
</Button>
</div>
</div>
<Alert className="bg-amber-50 border-amber-200">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-amber-800">Recommendation</AlertTitle>
<AlertDescription className="text-amber-700">
Consider selling your {crop} at {bestMarket?.name} within the next 7 days to maximize profit.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,88 @@
"use client";
import { Table, TableBody, TableHeader, TableRow, TableHead, TableCell } from "@/components/ui/table";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { ArrowUpRight, ArrowDownRight } from "lucide-react";
import { IMarketData, ITrend } from "@/lib/marketData";
interface MarketPriceTableProps {
marketData: IMarketData[];
isLoading: boolean;
onSelectCrop: (crop: string) => void;
}
const getTrendColor = (trend: ITrend) => (trend.direction === "up" ? "text-green-600" : "text-red-600");
const getTrendIcon = (trend: ITrend) =>
trend.direction === "up" ? (
<ArrowUpRight className="h-4 w-4 text-green-600" />
) : (
<ArrowDownRight className="h-4 w-4 text-red-600" />
);
export default function MarketPriceTable({ marketData, isLoading, onSelectCrop }: MarketPriceTableProps) {
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle>Market Price Table</CardTitle>
<CardDescription>Comprehensive price data across all markets and crops</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>Market Price Table</CardTitle>
<CardDescription>Comprehensive price data across all markets and crops</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Crop</TableHead>
{marketData[0]?.marketPrices.map((market) => (
<TableHead key={market.market}>{market.market}</TableHead>
))}
<TableHead>Average</TableHead>
<TableHead>Recommended</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{marketData.map((crop) => (
<TableRow
key={crop.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => onSelectCrop(crop.name)}>
<TableCell className="font-medium">{crop.name}</TableCell>
{crop.marketPrices.map((market) => (
<TableCell key={market.market}>
<div className="flex items-center gap-1">
<span>${market.price.toFixed(2)}</span>
<span className={getTrendColor(market.trend)}>{getTrendIcon(market.trend)}</span>
</div>
</TableCell>
))}
<TableCell>${crop.averagePrice.toFixed(2)}</TableCell>
<TableCell className="font-medium text-amber-600">${crop.recommendedPrice.toFixed(2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,64 @@
// components/MarketSummary.tsx
"use client";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import { ArrowUpRight, ArrowDownRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { IMarketData } from "@/lib/marketData";
interface MarketSummaryProps {
marketData: IMarketData[];
isLoading: boolean;
onSelectCrop: (crop: string) => void;
}
export default function MarketSummary({ marketData, isLoading, onSelectCrop }: MarketSummaryProps) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Market Summary</CardTitle>
<CardDescription>Today's market overview</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-5/6" />
</div>
) : (
<ScrollArea className="h-[220px] pr-4">
<div className="space-y-4">
{marketData.slice(0, 5).map((crop) => (
<div key={crop.id} className="flex items-center justify-between">
<div>
<p className="font-medium">{crop.name}</p>
<div className="flex items-center text-sm text-muted-foreground">
<span>${crop.averagePrice.toFixed(2)}</span>
{crop.marketPrices[0].trend.direction === "up" ? (
<span className="flex items-center text-green-600 ml-1">
<ArrowUpRight className="h-3 w-3 mr-0.5" />
{crop.marketPrices[0].trend.value}%
</span>
) : (
<span className="flex items-center text-red-600 ml-1">
<ArrowDownRight className="h-3 w-3 mr-0.5" />
{crop.marketPrices[0].trend.value}%
</span>
)}
</div>
</div>
<Button variant="ghost" size="sm" className="h-8" onClick={() => onSelectCrop(crop.name)}>
View
</Button>
</div>
))}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,130 @@
"use client";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { Card, CardContent } from "@/components/ui/card";
import { RefreshCw, Leaf, Calendar } from "lucide-react";
import CustomTooltip from "./CustomTooltip";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { IHistoricalData } from "@/lib/marketData";
interface PriceAnalyticsProps {
historicalData: IHistoricalData[];
isLoading: boolean;
selectedCrop: string;
}
export default function PriceAnalytics({ historicalData, isLoading, selectedCrop }: PriceAnalyticsProps) {
if (isLoading) {
return (
<div className="w-full h-[300px] flex items-center justify-center">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto text-primary/70" />
<p className="mt-2 text-sm text-muted-foreground">Loading market data...</p>
</div>
</div>
);
}
const currentPrice = historicalData[historicalData.length - 1]?.price ?? 0;
const averagePrice = historicalData.reduce((sum, item) => sum + item.price, 0) / historicalData.length;
const recommendedPrice = currentPrice * 1.05;
return (
<>
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={historicalData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis
dataKey="date"
tick={{ fontSize: 12 }}
tickFormatter={(value) => {
const date = new Date(value);
return `${date.getMonth() + 1}/${date.getDate()}`;
}}
/>
<YAxis
tick={{ fontSize: 12 }}
tickFormatter={(value) => `$${value}`}
domain={["dataMin - 0.5", "dataMax + 0.5"]}
/>
<YAxis
yAxisId={1}
orientation="right"
tick={{ fontSize: 12 }}
hide={true}
domain={["dataMin", "dataMax"]}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
<Line
type="monotone"
dataKey="price"
stroke="#16a34a"
strokeWidth={2}
dot={{ r: 2 }}
activeDot={{ r: 6 }}
name="Price per unit"
/>
<Line
type="monotone"
dataKey="volume"
stroke="#9ca3af"
strokeWidth={1.5}
strokeDasharray="5 5"
dot={false}
name="Trading volume"
yAxisId={1}
hide={true}
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4">
<Card className="bg-green-50 dark:bg-green-950/20">
<CardContent className="p-4">
<div className="flex justify-between items-center">
<div>
<p className="text-sm font-medium text-muted-foreground">Current Price</p>
<p className="text-2xl font-bold text-green-700">${currentPrice.toFixed(2)}</p>
</div>
<div className="h-10 w-10 rounded-full bg-green-100 flex items-center justify-center">
<Leaf className="h-5 w-5 text-green-700" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex justify-between items-center">
<div>
<p className="text-sm font-medium text-muted-foreground">30-Day Average</p>
<p className="text-2xl font-bold">${averagePrice.toFixed(2)}</p>
</div>
<div className="h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center">
<Calendar className="h-5 w-5 text-gray-700" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex justify-between items-center">
<div>
<p className="text-sm font-medium text-muted-foreground">Recommended Price</p>
<p className="text-2xl font-bold text-amber-600">${recommendedPrice.toFixed(2)}</p>
</div>
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200">
+5% margin
</Badge>
</div>
</CardContent>
</Card>
</div>
</>
);
}

View File

@ -0,0 +1,114 @@
// components/PriceAnalyticsCard.tsx
"use client";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { RefreshCw } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import PriceAnalytics from "./PriceAnalytics";
import MarketComparisonTab from "./MarketComparisonTab";
import DemandAnalysis from "./DemandAnalysis";
interface PriceAnalyticsCardProps {
isLoading: boolean;
selectedCrop: string;
timeRange: string;
lastUpdated: Date;
onSelectCrop: (crop: string) => void;
onTimeRangeChange: (value: string) => void;
onRefresh: () => void;
historicalData: any;
marketComparison: any;
}
export default function PriceAnalyticsCard({
isLoading,
selectedCrop,
timeRange,
lastUpdated,
onSelectCrop,
onTimeRangeChange,
onRefresh,
historicalData,
marketComparison,
}: PriceAnalyticsCardProps) {
return (
<Card className="md:col-span-3">
<CardHeader className="pb-2">
<div className="flex flex-col md:flex-row justify-between md:items-center gap-4">
<div>
<CardTitle>Price Analytics</CardTitle>
<CardDescription>Track price trends and market movements</CardDescription>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<Select value={selectedCrop} onValueChange={onSelectCrop}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select crop" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Corn">Corn</SelectItem>
<SelectItem value="Wheat">Wheat</SelectItem>
<SelectItem value="Soybeans">Soybeans</SelectItem>
<SelectItem value="Rice">Rice</SelectItem>
<SelectItem value="Potatoes">Potatoes</SelectItem>
<SelectItem value="Tomatoes">Tomatoes</SelectItem>
<SelectItem value="Apples">Apples</SelectItem>
<SelectItem value="Oranges">Oranges</SelectItem>
</SelectContent>
</Select>
<Select value={timeRange} onValueChange={onTimeRangeChange}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Time range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">7 days</SelectItem>
<SelectItem value="14">14 days</SelectItem>
<SelectItem value="30">30 days</SelectItem>
<SelectItem value="90">90 days</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="w-full h-[300px] flex items-center justify-center">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto text-primary/70" />
<p className="mt-2 text-sm text-muted-foreground">Loading market data...</p>
</div>
</div>
) : (
<Tabs defaultValue="price">
<TabsList className="mb-4">
<TabsTrigger value="price" className="gap-1">
Price Trend
</TabsTrigger>
<TabsTrigger value="comparison" className="gap-1">
Market Comparison
</TabsTrigger>
<TabsTrigger value="demand" className="gap-1">
Demand Analysis
</TabsTrigger>
</TabsList>
<TabsContent value="price" className="mt-0">
<PriceAnalytics historicalData={historicalData} isLoading={isLoading} selectedCrop={selectedCrop} />
</TabsContent>
<TabsContent value="comparison" className="mt-0">
<MarketComparisonTab
marketComparison={marketComparison}
selectedCrop={selectedCrop}
onSelectCrop={onSelectCrop}
/>
</TabsContent>
<TabsContent value="demand" className="mt-0">
<DemandAnalysis selectedCrop={selectedCrop} marketComparison={marketComparison} />
</TabsContent>
</Tabs>
)}
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,78 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { TrendingUp } from "lucide-react";
import { IMarketData } from "@/lib/marketData";
interface TopOpportunitiesProps {
marketData: IMarketData[];
isLoading: boolean;
onSelectCrop: (crop: string) => void;
}
export default function TopOpportunities({ marketData, isLoading, onSelectCrop }: TopOpportunitiesProps) {
if (isLoading) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Top Opportunities</CardTitle>
<CardDescription>Best selling opportunities today</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-5/6" />
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Top Opportunities</CardTitle>
<CardDescription>Best selling opportunities today</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{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 (
<div key={crop.id} className="border rounded-lg p-3 bg-green-50/50 dark:bg-green-950/10">
<div className="flex justify-between items-start">
<div>
<p className="font-medium">{crop.name}</p>
<p className="text-sm text-muted-foreground">
{bestMarket.market} - ${bestMarket.price.toFixed(2)}
</p>
</div>
<Badge className="bg-green-100 text-green-800 hover:bg-green-200">High Demand</Badge>
</div>
<div className="mt-2 flex justify-between items-center">
<div className="flex items-center gap-1">
<TrendingUp className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600">Recommended</span>
</div>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => onSelectCrop(crop.name)}>
View Details
</Button>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
);
}

View File

@ -2,26 +2,9 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
Card, import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
CardContent, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
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 { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
@ -35,7 +18,7 @@ import {
ResponsiveContainer, ResponsiveContainer,
BarChart, BarChart,
Bar, Bar,
Cell Cell,
} from "recharts"; } from "recharts";
import { import {
ArrowUpRight, ArrowUpRight,
@ -49,26 +32,14 @@ import {
Leaf, Leaf,
BarChart3, BarChart3,
LineChartIcon, LineChartIcon,
PieChart PieChart,
} from "lucide-react"; } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
Alert,
AlertDescription,
AlertTitle
} from "@/components/ui/alert";
// Define types for market data // Define types for market data
@ -121,26 +92,10 @@ interface MarketOpportunityProps {
// Mock data for market prices // Mock data for market prices
const generateMarketData = (): IMarketData[] => { const generateMarketData = (): IMarketData[] => {
const crops = [ const crops = ["Corn", "Wheat", "Soybeans", "Rice", "Potatoes", "Tomatoes", "Apples", "Oranges"];
"Corn", const markets = ["National Market", "Regional Hub", "Local Market", "Export Market", "Wholesale Market"];
"Wheat",
"Soybeans",
"Rice",
"Potatoes",
"Tomatoes",
"Apples",
"Oranges"
];
const markets = [
"National Market",
"Regional Hub",
"Local Market",
"Export Market",
"Wholesale Market"
];
const getRandomPrice = (base: number) => const getRandomPrice = (base: number) => Number((base + Math.random() * 2).toFixed(2));
Number((base + Math.random() * 2).toFixed(2));
const getRandomDemand = () => Math.floor(Math.random() * 100); const getRandomDemand = () => Math.floor(Math.random() * 100);
const getRandomTrend = (): ITrend => const getRandomTrend = (): ITrend =>
Math.random() > 0.5 Math.random() > 0.5
@ -172,21 +127,18 @@ const generateMarketData = (): IMarketData[] => {
market, market,
price: getRandomPrice(basePrice), price: getRandomPrice(basePrice),
demand: getRandomDemand(), demand: getRandomDemand(),
trend: getRandomTrend() trend: getRandomTrend(),
})), })),
averagePrice: getRandomPrice(basePrice - 0.5), averagePrice: getRandomPrice(basePrice - 0.5),
recommendedPrice: getRandomPrice(basePrice + 0.2), recommendedPrice: getRandomPrice(basePrice + 0.2),
demandScore: getRandomDemand(), demandScore: getRandomDemand(),
opportunity: Math.random() > 0.7 opportunity: Math.random() > 0.7,
}; };
}); });
}; };
// Generate historical price data for a specific crop // Generate historical price data for a specific crop
const generateHistoricalData = ( const generateHistoricalData = (crop: string, days = 30): IHistoricalData[] => {
crop: string,
days = 30
): IHistoricalData[] => {
const basePrice = const basePrice =
crop === "Corn" crop === "Corn"
? 4 ? 4
@ -216,7 +168,7 @@ const generateHistoricalData = (
data.push({ data.push({
date: date.toISOString().split("T")[0], date: date.toISOString().split("T")[0],
price: Number(currentPrice.toFixed(2)), price: Number(currentPrice.toFixed(2)),
volume: Math.floor(Math.random() * 1000) + 200 volume: Math.floor(Math.random() * 1000) + 200,
}); });
} }
@ -224,16 +176,8 @@ const generateHistoricalData = (
}; };
// Generate market comparison data // Generate market comparison data
const generateMarketComparisonData = ( const generateMarketComparisonData = (crop: string): IMarketComparison[] => {
crop: string const markets = ["National Market", "Regional Hub", "Local Market", "Export Market", "Wholesale Market"];
): IMarketComparison[] => {
const markets = [
"National Market",
"Regional Hub",
"Local Market",
"Export Market",
"Wholesale Market"
];
const basePrice = const basePrice =
crop === "Corn" crop === "Corn"
? 4 ? 4
@ -254,26 +198,18 @@ const generateMarketComparisonData = (
return markets.map((market) => ({ return markets.map((market) => ({
name: market, name: market,
price: Number((basePrice + (Math.random() - 0.5) * 2).toFixed(2)), price: Number((basePrice + (Math.random() - 0.5) * 2).toFixed(2)),
demand: Math.floor(Math.random() * 100) demand: Math.floor(Math.random() * 100),
})); }));
}; };
// Custom tooltip for the price chart // Custom tooltip for the price chart
const CustomTooltip = ({ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
active,
payload,
label
}: CustomTooltipProps) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<Card className="bg-white dark:bg-gray-800 p-2 shadow-md border-none"> <Card className="bg-white dark:bg-gray-800 p-2 shadow-md border-none">
<p className="text-sm font-medium">{label}</p> <p className="text-sm font-medium">{label}</p>
<p className="text-sm text-primary">Price: ${payload[0].value}</p> <p className="text-sm text-primary">Price: ${payload[0].value}</p>
{payload[1] && ( {payload[1] && <p className="text-sm text-gray-500">Volume: {payload[1].value} units</p>}
<p className="text-sm text-gray-500">
Volume: {payload[1].value} units
</p>
)}
</Card> </Card>
); );
} }
@ -285,9 +221,7 @@ const MarketOpportunity = ({ crop, data }: MarketOpportunityProps) => {
const highestPrice = Math.max(...data.map((item) => item.price)); const highestPrice = Math.max(...data.map((item) => item.price));
const bestMarket = data.find((item) => item.price === highestPrice); const bestMarket = data.find((item) => item.price === highestPrice);
const highestDemand = Math.max(...data.map((item) => item.demand)); const highestDemand = Math.max(...data.map((item) => item.demand));
const highDemandMarket = data.find( const highDemandMarket = data.find((item) => item.demand === highestDemand);
(item) => item.demand === highestDemand
);
return ( return (
<Card className="border-l-4 border-l-green-500"> <Card className="border-l-4 border-l-green-500">
@ -296,28 +230,19 @@ const MarketOpportunity = ({ crop, data }: MarketOpportunityProps) => {
<TrendingUp className="h-5 w-5 mr-2 text-green-500" /> <TrendingUp className="h-5 w-5 mr-2 text-green-500" />
Sales Opportunity for {crop} Sales Opportunity for {crop}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>Based on current market conditions</CardDescription>
Based on current market conditions
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="text-sm font-medium mb-1"> <p className="text-sm font-medium mb-1">Best Price Opportunity</p>
Best Price Opportunity
</p>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-lg font-bold text-green-600"> <p className="text-lg font-bold text-green-600">${bestMarket?.price}</p>
${bestMarket?.price} <p className="text-sm text-muted-foreground">{bestMarket?.name}</p>
</p>
<p className="text-sm text-muted-foreground">
{bestMarket?.name}
</p>
</div> </div>
<Badge className="bg-green-100 text-green-800 hover:bg-green-200"> <Badge className="bg-green-100 text-green-800 hover:bg-green-200">
{Math.round((bestMarket!.price / (highestPrice - 1)) * 100)}% {Math.round((bestMarket!.price / (highestPrice - 1)) * 100)}% above average
above average
</Badge> </Badge>
</div> </div>
</div> </div>
@ -325,22 +250,13 @@ const MarketOpportunity = ({ crop, data }: MarketOpportunityProps) => {
<Separator /> <Separator />
<div> <div>
<p className="text-sm font-medium mb-1"> <p className="text-sm font-medium mb-1">Highest Demand</p>
Highest Demand
</p>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-lg font-bold"> <p className="text-lg font-bold">{highDemandMarket?.name}</p>
{highDemandMarket?.name}
</p>
<div className="flex items-center"> <div className="flex items-center">
<Progress <Progress value={highDemandMarket?.demand} className="h-2 w-24 mr-2" />
value={highDemandMarket?.demand} <span className="text-sm">{highDemandMarket?.demand}% demand</span>
className="h-2 w-24 mr-2"
/>
<span className="text-sm">
{highDemandMarket?.demand}% demand
</span>
</div> </div>
</div> </div>
<Button variant="outline" size="sm" className="gap-1"> <Button variant="outline" size="sm" className="gap-1">
@ -351,12 +267,9 @@ const MarketOpportunity = ({ crop, data }: MarketOpportunityProps) => {
<Alert className="bg-amber-50 border-amber-200"> <Alert className="bg-amber-50 border-amber-200">
<AlertCircle className="h-4 w-4 text-amber-600" /> <AlertCircle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-amber-800"> <AlertTitle className="text-amber-800">Recommendation</AlertTitle>
Recommendation
</AlertTitle>
<AlertDescription className="text-amber-700"> <AlertDescription className="text-amber-700">
Consider selling your {crop} at {bestMarket?.name} within Consider selling your {crop} at {bestMarket?.name} within the next 7 days to maximize profit.
the next 7 days to maximize profit.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -374,17 +287,14 @@ export default function MarketplacePage() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [marketData, setMarketData] = useState<IMarketData[]>([]); const [marketData, setMarketData] = useState<IMarketData[]>([]);
const [historicalData, setHistoricalData] = useState<IHistoricalData[]>([]); const [historicalData, setHistoricalData] = useState<IHistoricalData[]>([]);
const [marketComparison, setMarketComparison] = const [marketComparison, setMarketComparison] = useState<IMarketComparison[]>([]);
useState<IMarketComparison[]>([]);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date()); const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setMarketData(generateMarketData()); setMarketData(generateMarketData());
setHistoricalData( setHistoricalData(generateHistoricalData(selectedCrop, Number.parseInt(timeRange)));
generateHistoricalData(selectedCrop, Number.parseInt(timeRange))
);
setMarketComparison(generateMarketComparisonData(selectedCrop)); setMarketComparison(generateMarketComparisonData(selectedCrop));
setLastUpdated(new Date()); setLastUpdated(new Date());
setIsLoading(false); setIsLoading(false);
@ -397,9 +307,7 @@ export default function MarketplacePage() {
setIsLoading(true); setIsLoading(true);
setTimeout(() => { setTimeout(() => {
setMarketData(generateMarketData()); setMarketData(generateMarketData());
setHistoricalData( setHistoricalData(generateHistoricalData(selectedCrop, Number.parseInt(timeRange)));
generateHistoricalData(selectedCrop, Number.parseInt(timeRange))
);
setMarketComparison(generateMarketComparisonData(selectedCrop)); setMarketComparison(generateMarketComparisonData(selectedCrop));
setLastUpdated(new Date()); setLastUpdated(new Date());
setIsLoading(false); setIsLoading(false);
@ -424,33 +332,18 @@ export default function MarketplacePage() {
<div className="container mx-auto py-6 px-4 md:px-6"> <div className="container mx-auto py-6 px-4 md:px-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight"> <h1 className="text-3xl font-bold tracking-tight">Marketplace Information</h1>
Marketplace Information
</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
Make informed decisions with real-time market data and price Make informed decisions with real-time market data and price analytics
analytics
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button variant="outline" size="sm" className="gap-1" onClick={handleRefresh} disabled={isLoading}>
variant="outline" <RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
size="sm"
className="gap-1"
onClick={handleRefresh}
disabled={isLoading}
>
<RefreshCw
className={`h-4 w-4 ${
isLoading ? "animate-spin" : ""
}`}
/>
{isLoading ? "Updating..." : "Refresh Data"} {isLoading ? "Updating..." : "Refresh Data"}
</Button> </Button>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">Last updated: {lastUpdated.toLocaleTimeString()}</div>
Last updated: {lastUpdated.toLocaleTimeString()}
</div>
</div> </div>
</div> </div>
@ -460,9 +353,7 @@ export default function MarketplacePage() {
<div className="flex flex-col md:flex-row justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row justify-between md:items-center gap-4">
<div> <div>
<CardTitle>Price Analytics</CardTitle> <CardTitle>Price Analytics</CardTitle>
<CardDescription> <CardDescription>Track price trends and market movements</CardDescription>
Track price trends and market movements
</CardDescription>
</div> </div>
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col sm:flex-row gap-2">
<Select value={selectedCrop} onValueChange={setSelectedCrop}> <Select value={selectedCrop} onValueChange={setSelectedCrop}>
@ -500,9 +391,7 @@ export default function MarketplacePage() {
<div className="w-full h-[300px] flex items-center justify-center"> <div className="w-full h-[300px] flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto text-primary/70" /> <RefreshCw className="h-8 w-8 animate-spin mx-auto text-primary/70" />
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">Loading market data...</p>
Loading market data...
</p>
</div> </div>
</div> </div>
) : ( ) : (
@ -528,21 +417,15 @@ export default function MarketplacePage() {
top: 5, top: 5,
right: 30, right: 30,
left: 20, left: 20,
bottom: 5 bottom: 5,
}} }}>
> <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<CartesianGrid
strokeDasharray="3 3"
stroke="#e5e7eb"
/>
<XAxis <XAxis
dataKey="date" dataKey="date"
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
tickFormatter={(value) => { tickFormatter={(value) => {
const date = new Date(value); const date = new Date(value);
return `${date.getMonth() + 1}/${ return `${date.getMonth() + 1}/${date.getDate()}`;
date.getDate()
}`;
}} }}
/> />
<YAxis <YAxis
@ -588,14 +471,9 @@ export default function MarketplacePage() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<p className="text-sm font-medium text-muted-foreground"> <p className="text-sm font-medium text-muted-foreground">Current Price</p>
Current Price
</p>
<p className="text-2xl font-bold text-green-700"> <p className="text-2xl font-bold text-green-700">
$ ${historicalData[historicalData.length - 1]?.price.toFixed(2)}
{historicalData[
historicalData.length - 1
]?.price.toFixed(2)}
</p> </p>
</div> </div>
<div className="h-10 w-10 rounded-full bg-green-100 flex items-center justify-center"> <div className="h-10 w-10 rounded-full bg-green-100 flex items-center justify-center">
@ -609,16 +487,11 @@ export default function MarketplacePage() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<p className="text-sm font-medium text-muted-foreground"> <p className="text-sm font-medium text-muted-foreground">30-Day Average</p>
30-Day Average
</p>
<p className="text-2xl font-bold"> <p className="text-2xl font-bold">
$ $
{( {(
historicalData.reduce( historicalData.reduce((sum, item) => sum + item.price, 0) / historicalData.length
(sum, item) => sum + item.price,
0
) / historicalData.length
).toFixed(2)} ).toFixed(2)}
</p> </p>
</div> </div>
@ -633,21 +506,12 @@ export default function MarketplacePage() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<p className="text-sm font-medium text-muted-foreground"> <p className="text-sm font-medium text-muted-foreground">Recommended Price</p>
Recommended Price
</p>
<p className="text-2xl font-bold text-amber-600"> <p className="text-2xl font-bold text-amber-600">
$ ${(historicalData[historicalData.length - 1]?.price * 1.05).toFixed(2)}
{(
historicalData[historicalData.length - 1]
?.price * 1.05
).toFixed(2)}
</p> </p>
</div> </div>
<Badge <Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200">
variant="outline"
className="bg-amber-50 text-amber-700 border-amber-200"
>
+5% margin +5% margin
</Badge> </Badge>
</div> </div>
@ -665,36 +529,19 @@ export default function MarketplacePage() {
top: 5, top: 5,
right: 30, right: 30,
left: 20, left: 20,
bottom: 5 bottom: 5,
}} }}>
> <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<CartesianGrid
strokeDasharray="3 3"
stroke="#e5e7eb"
/>
<XAxis dataKey="name" tick={{ fontSize: 12 }} /> <XAxis dataKey="name" tick={{ fontSize: 12 }} />
<YAxis <YAxis tick={{ fontSize: 12 }} tickFormatter={(value) => `$${value}`} />
tick={{ fontSize: 12 }}
tickFormatter={(value) => `$${value}`}
/>
<Tooltip /> <Tooltip />
<Legend /> <Legend />
<Bar <Bar dataKey="price" name="Price per unit" fill="#16a34a" radius={[4, 4, 0, 0]}>
dataKey="price"
name="Price per unit"
fill="#16a34a"
radius={[4, 4, 0, 0]}
>
{marketComparison.map((entry, index) => ( {marketComparison.map((entry, index) => (
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}
fill={ fill={
entry.price === entry.price === Math.max(...marketComparison.map((item) => item.price))
Math.max(
...marketComparison.map(
(item) => item.price
)
)
? "#15803d" ? "#15803d"
: "#16a34a" : "#16a34a"
} }
@ -708,8 +555,7 @@ export default function MarketplacePage() {
<div className="mt-4"> <div className="mt-4">
<Table> <Table>
<TableCaption> <TableCaption>
Market comparison for {selectedCrop} as of{" "} Market comparison for {selectedCrop} as of {new Date().toLocaleDateString()}
{new Date().toLocaleDateString()}
</TableCaption> </TableCaption>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -717,54 +563,30 @@ export default function MarketplacePage() {
<TableHead>Price</TableHead> <TableHead>Price</TableHead>
<TableHead>Demand</TableHead> <TableHead>Demand</TableHead>
<TableHead>Price Difference</TableHead> <TableHead>Price Difference</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">Action</TableHead>
Action
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{marketComparison.map((market) => { {marketComparison.map((market) => {
const avgPrice = const avgPrice =
marketComparison.reduce( marketComparison.reduce((sum, m) => sum + m.price, 0) / marketComparison.length;
(sum, m) => sum + m.price, const priceDiff = (((market.price - avgPrice) / avgPrice) * 100).toFixed(1);
0
) / marketComparison.length;
const priceDiff = (
((market.price - avgPrice) / avgPrice) *
100
).toFixed(1);
const isPriceHigh = Number.parseFloat(priceDiff) > 0; const isPriceHigh = Number.parseFloat(priceDiff) > 0;
return ( return (
<TableRow <TableRow
key={market.name} key={market.name}
className="cursor-pointer hover:bg-muted/50" className="cursor-pointer hover:bg-muted/50"
onClick={() => onClick={() => setSelectedCrop(market.name)}>
setSelectedCrop(market.name) <TableCell className="font-medium">{market.name}</TableCell>
} <TableCell>${market.price.toFixed(2)}</TableCell>
>
<TableCell className="font-medium">
{market.name}
</TableCell>
<TableCell>
${market.price.toFixed(2)}
</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Progress <Progress value={market.demand} className="h-2 w-16" />
value={market.demand}
className="h-2 w-16"
/>
<span>{market.demand}%</span> <span>{market.demand}%</span>
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell className={isPriceHigh ? "text-green-600" : "text-red-600"}>
className={
isPriceHigh
? "text-green-600"
: "text-red-600"
}
>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{isPriceHigh ? ( {isPriceHigh ? (
<ArrowUpRight className="h-4 w-4" /> <ArrowUpRight className="h-4 w-4" />
@ -775,11 +597,7 @@ export default function MarketplacePage() {
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <Button variant="ghost" size="sm" className="h-8 gap-1">
variant="ghost"
size="sm"
className="h-8 gap-1"
>
Details <ChevronRight className="h-4 w-4" /> Details <ChevronRight className="h-4 w-4" />
</Button> </Button>
</TableCell> </TableCell>
@ -795,12 +613,8 @@ export default function MarketplacePage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-lg"> <CardTitle className="text-lg">Demand Forecast</CardTitle>
Demand Forecast <CardDescription>Projected demand for {selectedCrop} over the next 30 days</CardDescription>
</CardTitle>
<CardDescription>
Projected demand for {selectedCrop} over the next 30 days
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
@ -808,9 +622,7 @@ export default function MarketplacePage() {
<div key={market.name} className="space-y-1"> <div key={market.name} className="space-y-1">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span>{market.name}</span> <span>{market.name}</span>
<span className="font-medium"> <span className="font-medium">{market.demand}%</span>
{market.demand}%
</span>
</div> </div>
<Progress value={market.demand} className="h-2" /> <Progress value={market.demand} className="h-2" />
</div> </div>
@ -831,9 +643,7 @@ export default function MarketplacePage() {
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-lg">Market Summary</CardTitle> <CardTitle className="text-lg">Market Summary</CardTitle>
<CardDescription> <CardDescription>Today&apos;s market overview</CardDescription>
Today&apos;s market overview
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@ -846,10 +656,7 @@ export default function MarketplacePage() {
<ScrollArea className="h-[220px] pr-4"> <ScrollArea className="h-[220px] pr-4">
<div className="space-y-4"> <div className="space-y-4">
{marketData.slice(0, 5).map((crop) => ( {marketData.slice(0, 5).map((crop) => (
<div <div key={crop.id} className="flex items-center justify-between">
key={crop.id}
className="flex items-center justify-between"
>
<div> <div>
<p className="font-medium">{crop.name}</p> <p className="font-medium">{crop.name}</p>
<div className="flex items-center text-sm text-muted-foreground"> <div className="flex items-center text-sm text-muted-foreground">
@ -867,12 +674,7 @@ export default function MarketplacePage() {
)} )}
</div> </div>
</div> </div>
<Button <Button variant="ghost" size="sm" className="h-8" onClick={() => setSelectedCrop(crop.name)}>
variant="ghost"
size="sm"
className="h-8"
onClick={() => setSelectedCrop(crop.name)}
>
View View
</Button> </Button>
</div> </div>
@ -886,9 +688,7 @@ export default function MarketplacePage() {
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-lg">Top Opportunities</CardTitle> <CardTitle className="text-lg">Top Opportunities</CardTitle>
<CardDescription> <CardDescription>Best selling opportunities today</CardDescription>
Best selling opportunities today
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@ -904,40 +704,30 @@ export default function MarketplacePage() {
.slice(0, 3) .slice(0, 3)
.map((crop) => { .map((crop) => {
const bestMarket = crop.marketPrices.reduce( const bestMarket = crop.marketPrices.reduce(
(best, current) => (best, current) => (current.price > best.price ? current : best),
current.price > best.price ? current : best,
crop.marketPrices[0] crop.marketPrices[0]
); );
return ( return (
<div <div key={crop.id} className="border rounded-lg p-3 bg-green-50/50 dark:bg-green-950/10">
key={crop.id}
className="border rounded-lg p-3 bg-green-50/50 dark:bg-green-950/10"
>
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<p className="font-medium">{crop.name}</p> <p className="font-medium">{crop.name}</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{bestMarket.market} - $ {bestMarket.market} - ${bestMarket.price.toFixed(2)}
{bestMarket.price.toFixed(2)}
</p> </p>
</div> </div>
<Badge className="bg-green-100 text-green-800 hover:bg-green-200"> <Badge className="bg-green-100 text-green-800 hover:bg-green-200">High Demand</Badge>
High Demand
</Badge>
</div> </div>
<div className="mt-2 flex justify-between items-center"> <div className="mt-2 flex justify-between items-center">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<TrendingUp className="h-4 w-4 text-green-600" /> <TrendingUp className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600"> <span className="text-sm text-green-600">Recommended</span>
Recommended
</span>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-7 text-xs" className="h-7 text-xs"
onClick={() => setSelectedCrop(crop.name)} onClick={() => setSelectedCrop(crop.name)}>
>
View Details View Details
</Button> </Button>
</div> </div>
@ -954,9 +744,7 @@ export default function MarketplacePage() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Market Price Table</CardTitle> <CardTitle>Market Price Table</CardTitle>
<CardDescription> <CardDescription>Comprehensive price data across all markets and crops</CardDescription>
Comprehensive price data across all markets and crops
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@ -973,9 +761,7 @@ export default function MarketplacePage() {
<TableRow> <TableRow>
<TableHead>Crop</TableHead> <TableHead>Crop</TableHead>
{marketData[0]?.marketPrices.map((market) => ( {marketData[0]?.marketPrices.map((market) => (
<TableHead key={market.market}> <TableHead key={market.market}>{market.market}</TableHead>
{market.market}
</TableHead>
))} ))}
<TableHead>Average</TableHead> <TableHead>Average</TableHead>
<TableHead>Recommended</TableHead> <TableHead>Recommended</TableHead>
@ -986,27 +772,18 @@ export default function MarketplacePage() {
<TableRow <TableRow
key={crop.id} key={crop.id}
className="cursor-pointer hover:bg-muted/50" className="cursor-pointer hover:bg-muted/50"
onClick={() => setSelectedCrop(crop.name)} onClick={() => setSelectedCrop(crop.name)}>
> <TableCell className="font-medium">{crop.name}</TableCell>
<TableCell className="font-medium">
{crop.name}
</TableCell>
{crop.marketPrices.map((market) => ( {crop.marketPrices.map((market) => (
<TableCell key={market.market}> <TableCell key={market.market}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>${market.price.toFixed(2)}</span> <span>${market.price.toFixed(2)}</span>
<span className={getTrendColor(market.trend)}> <span className={getTrendColor(market.trend)}>{getTrendIcon(market.trend)}</span>
{getTrendIcon(market.trend)}
</span>
</div> </div>
</TableCell> </TableCell>
))} ))}
<TableCell> <TableCell>${crop.averagePrice.toFixed(2)}</TableCell>
${crop.averagePrice.toFixed(2)} <TableCell className="font-medium text-amber-600">${crop.recommendedPrice.toFixed(2)}</TableCell>
</TableCell>
<TableCell className="font-medium text-amber-600">
${crop.recommendedPrice.toFixed(2)}
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@ -1017,4 +794,4 @@ export default function MarketplacePage() {
</Card> </Card>
</div> </div>
); );
} }

View File

@ -20,17 +20,37 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
type harvestSchema = z.infer<typeof harvestDetailsFormSchema>; type harvestSchema = z.infer<typeof harvestDetailsFormSchema>;
export default function HarvestDetailsForm() { export default function HarvestDetailsForm({
onChange,
}: {
onChange: (data: harvestSchema) => void;
}) {
const form = useForm<harvestSchema>({ const form = useForm<harvestSchema>({
resolver: zodResolver(harvestDetailsFormSchema), resolver: zodResolver(harvestDetailsFormSchema),
defaultValues: {}, defaultValues: {
daysToFlower: 0,
daysToMaturity: 0,
harvestWindow: 0,
estimatedLossRate: 0,
harvestUnits: "",
estimatedRevenue: 0,
expectedYieldPer100ft: 0,
expectedYieldPerAcre: 0,
},
}); });
const onSubmit: (data: harvestSchema) => void = (data) => {
onChange(data);
};
return ( return (
<Form {...form}> <Form {...form}>
<form className="grid grid-cols-3 gap-5"> <form
className="grid grid-cols-3 gap-5"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField <FormField
control={form.control} control={form.control}
name="daysToFlower" name="daysToFlower"
@ -47,6 +67,13 @@ export default function HarvestDetailsForm() {
id="daysToFlower" id="daysToFlower"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -71,6 +98,13 @@ export default function HarvestDetailsForm() {
id="daysToMaturity" id="daysToMaturity"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -95,6 +129,13 @@ export default function HarvestDetailsForm() {
id="harvestWindow" id="harvestWindow"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -119,6 +160,13 @@ export default function HarvestDetailsForm() {
id="estimatedLossRate" id="estimatedLossRate"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -168,6 +216,13 @@ export default function HarvestDetailsForm() {
id="estimatedRevenue" id="estimatedRevenue"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -192,6 +247,13 @@ export default function HarvestDetailsForm() {
id="expectedYieldPer100ft" id="expectedYieldPer100ft"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -216,6 +278,13 @@ export default function HarvestDetailsForm() {
id="expectedYieldPerAcre" id="expectedYieldPerAcre"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -224,6 +293,14 @@ export default function HarvestDetailsForm() {
</FormItem> </FormItem>
)} )}
/> />
<div className="col-span-3 flex justify-center">
<Button
type="submit"
className="bg-blue-500 hover:bg-blue-600 duration-100"
>
Save
</Button>
</div>
</form> </form>
</Form> </Form>
); );

View File

@ -1,33 +1,140 @@
"use client";
import { useState } from "react";
import PlantingDetailsForm from "./planting-detail-form"; import PlantingDetailsForm from "./planting-detail-form";
import HarvestDetailsForm from "./harvest-detail-form"; import HarvestDetailsForm from "./harvest-detail-form";
import { Separator } from "@/components/ui/separator";
import GoogleMapWithDrawing from "@/components/google-map-with-drawing"; import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
import { Separator } from "@/components/ui/separator";
import {
plantingDetailsFormSchema,
harvestDetailsFormSchema,
} from "@/schemas/application.schema";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
type PlantingSchema = z.infer<typeof plantingDetailsFormSchema>;
type HarvestSchema = z.infer<typeof harvestDetailsFormSchema>;
const steps = [
{ title: "Step 1", description: "Planting Details" },
{ title: "Step 2", description: "Harvest Details" },
{ title: "Step 3", description: "Select Map Area" },
];
export default function SetupPage() { export default function SetupPage() {
const [step, setStep] = useState(1);
const [plantingDetails, setPlantingDetails] = useState<PlantingSchema | null>(
null
);
const [harvestDetails, setHarvestDetails] = useState<HarvestSchema | null>(
null
);
const [mapData, setMapData] = useState<{ lat: number; lng: number }[] | null>(
null
);
const handleNext = () => {
if (step === 1 && !plantingDetails) {
toast.warning(
"Please complete the Planting Details before proceeding.",
{
action: {
label: "Close",
onClick: () => toast.dismiss(),
},
}
);
return;
}
if (step === 2 && !harvestDetails) {
toast.warning(
"Please complete the Harvest Details before proceeding.",
{
action: {
label: "Close",
onClick: () => toast.dismiss(),
},
}
);
return;
}
setStep((prev) => prev + 1);
};
const handleBack = () => {
setStep((prev) => prev - 1);
};
const handleSubmit = () => {
if (!mapData) {
toast.warning("Please select an area on the map before submitting.", {
action: {
label: "Close",
onClick: () => toast.dismiss(),
},
});
return;
}
console.log("Submitting:", { plantingDetails, harvestDetails, mapData });
// send request to the server
};
return ( return (
<div className="p-5"> <div className="p-5">
<div className=" flex justify-center"> {/* Stepper Navigation */}
<h1 className="flex text-2xl ">Plating Details</h1> <div className="flex justify-between items-center mb-5">
{steps.map((item, index) => (
<div key={index} className="flex flex-col items-center">
<div
className={`w-10 h-10 flex items-center justify-center rounded-full text-white font-bold ${
step === index + 1 ? "bg-blue-500" : "bg-gray-500"
}`}
>
{index + 1}
</div>
<span className="font-medium mt-2">{item.title}</span>
<span className="text-gray-500 text-sm">{item.description}</span>
</div>
))}
</div> </div>
<Separator className="mt-3" />
<div className="mt-10 flex justify-center"> <Separator className="mb-5" />
<PlantingDetailsForm />
</div> {step === 1 && (
<div className=" flex justify-center mt-20"> <>
<h1 className="flex text-2xl ">Harvest Details</h1> <h2 className="text-xl text-center mb-5">Planting Details</h2>
</div> <PlantingDetailsForm onChange={setPlantingDetails} />
<Separator className="mt-3" /> </>
<div className="mt-10 flex justify-center"> )}
<HarvestDetailsForm />
</div> {step === 2 && (
<div className="mt-10"> <>
<div className=" flex justify-center mt-20"> <h2 className="text-xl text-center mb-5">Harvest Details</h2>
<h1 className="flex text-2xl ">Map</h1> <HarvestDetailsForm onChange={setHarvestDetails} />
</div> </>
<Separator className="mt-3" /> )}
<div className="mt-10">
<GoogleMapWithDrawing /> {step === 3 && (
</div> <>
<h2 className="text-xl text-center mb-5">Select Area on Map</h2>
<GoogleMapWithDrawing onAreaSelected={setMapData} />
</>
)}
<div className="mt-10 flex justify-between">
<Button onClick={handleBack} disabled={step === 1}>
Back
</Button>
{step < 3 ? (
<Button onClick={handleNext}>Next</Button>
) : (
<Button onClick={handleSubmit}>Submit</Button>
)}
</div> </div>
</div> </div>
); );

View File

@ -22,17 +22,45 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { useRef } from "react";
type plantingSchema = z.infer<typeof plantingDetailsFormSchema>; type plantingSchema = z.infer<typeof plantingDetailsFormSchema>;
export default function PlantingDetailsForm() { export default function PlantingDetailsForm({
const form = useForm<plantingSchema>({ onChange,
}: {
onChange: (data: plantingSchema) => void;
}) {
const formRef = useRef<HTMLFormElement>(null);
const form = useForm({
resolver: zodResolver(plantingDetailsFormSchema), resolver: zodResolver(plantingDetailsFormSchema),
defaultValues: {}, defaultValues: {
daysToEmerge: 0,
plantSpacing: 0,
rowSpacing: 0,
plantingDepth: 0,
averageHeight: 0,
startMethod: "",
lightProfile: "",
soilConditions: "",
plantingDetails: "",
pruningDetails: "",
isPerennial: false,
autoCreateTasks: false,
},
}); });
const onSubmit = (data: plantingSchema) => {
onChange(data);
};
return ( return (
<Form {...form}> <Form {...form}>
<form className="grid grid-cols-3 gap-5"> <form
className="grid grid-cols-3 gap-5"
onSubmit={form.handleSubmit(onSubmit)}
ref={formRef}
>
<FormField <FormField
control={form.control} control={form.control}
name="daysToEmerge" name="daysToEmerge"
@ -47,6 +75,13 @@ export default function PlantingDetailsForm() {
id="daysToEmerge" id="daysToEmerge"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -69,6 +104,13 @@ export default function PlantingDetailsForm() {
id="plantSpacing" id="plantSpacing"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -91,6 +133,13 @@ export default function PlantingDetailsForm() {
id="rowSpacing" id="rowSpacing"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -115,6 +164,13 @@ export default function PlantingDetailsForm() {
id="plantingDepth" id="plantingDepth"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -139,6 +195,13 @@ export default function PlantingDetailsForm() {
id="averageHeight" id="averageHeight"
className="w-96" className="w-96"
{...field} {...field}
onChange={(e) => {
// convert to number
const value = e.target.value
? parseInt(e.target.value, 10)
: "";
field.onChange(value);
}}
/> />
</div> </div>
</div> </div>
@ -187,9 +250,9 @@ export default function PlantingDetailsForm() {
<SelectValue placeholder="Select light profile" /> <SelectValue placeholder="Select light profile" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="xp">Seed</SelectItem> <SelectItem value="Seed">Seed</SelectItem>
<SelectItem value="xa">Transplant</SelectItem> <SelectItem value="Transplant">Transplant</SelectItem>
<SelectItem value="xz">Cutting</SelectItem> <SelectItem value="Cutting">Cutting</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@ -214,9 +277,9 @@ export default function PlantingDetailsForm() {
<SelectValue placeholder="Select a soil condition" /> <SelectValue placeholder="Select a soil condition" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="xp">Seed</SelectItem> <SelectItem value="Seed">Seed</SelectItem>
<SelectItem value="xa">Transplant</SelectItem> <SelectItem value="Transplant">Transplant</SelectItem>
<SelectItem value="xz">Cutting</SelectItem> <SelectItem value="Cutting">Cutting</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@ -289,7 +352,7 @@ export default function PlantingDetailsForm() {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="isPerennial" name="autoCreateTasks"
render={({ field }: { field: any }) => ( render={({ field }: { field: any }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
@ -308,6 +371,11 @@ export default function PlantingDetailsForm() {
</FormItem> </FormItem>
)} )}
/> />
<div className="col-span-3 flex justify-center">
<Button type="submit" className="bg-blue-500 hover:bg-blue-600 duration-100">
Save
</Button>
</div>
</form> </form>
</Form> </Form>
); );

View File

@ -1,5 +1,3 @@
"use client";
import { GoogleMap, LoadScript, DrawingManager } from "@react-google-maps/api"; import { GoogleMap, LoadScript, DrawingManager } from "@react-google-maps/api";
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
@ -10,17 +8,45 @@ const containerStyle = {
const center = { lat: 13.7563, lng: 100.5018 }; // Example: Bangkok, Thailand const center = { lat: 13.7563, lng: 100.5018 }; // Example: Bangkok, Thailand
const GoogleMapWithDrawing = () => { interface GoogleMapWithDrawingProps {
onAreaSelected: (data: { lat: number; lng: number }[]) => void;
}
const GoogleMapWithDrawing = ({
onAreaSelected,
}: GoogleMapWithDrawingProps) => {
const [map, setMap] = useState<google.maps.Map | null>(null); const [map, setMap] = useState<google.maps.Map | null>(null);
// Handles drawing complete const onDrawingComplete = useCallback(
const onDrawingComplete = useCallback((overlay: google.maps.drawing.OverlayCompleteEvent) => { (overlay: google.maps.drawing.OverlayCompleteEvent) => {
console.log("Drawing complete:", overlay); const shape = overlay.overlay;
}, []);
if (shape instanceof google.maps.Polyline) {
const path = shape.getPath();
const coordinates = path.getArray().map((latLng) => ({
lat: latLng.lat(),
lng: latLng.lng(),
}));
// console.log("Polyline coordinates:", coordinates);
onAreaSelected(coordinates);
} else {
console.log("Unknown shape detected:", shape);
}
},
[onAreaSelected]
);
return ( return (
<LoadScript googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!} libraries={["drawing"]}> <LoadScript
<GoogleMap mapContainerStyle={containerStyle} center={center} zoom={10} onLoad={(map) => setMap(map)}> googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}
libraries={["drawing"]}
>
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={10}
onLoad={(map) => setMap(map)}
>
{map && ( {map && (
<DrawingManager <DrawingManager
onOverlayComplete={onDrawingComplete} onOverlayComplete={onDrawingComplete}
@ -29,9 +55,6 @@ const GoogleMapWithDrawing = () => {
drawingControlOptions: { drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER, position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [ drawingModes: [
google.maps.drawing.OverlayType.POLYGON,
google.maps.drawing.OverlayType.RECTANGLE,
google.maps.drawing.OverlayType.CIRCLE,
google.maps.drawing.OverlayType.POLYLINE, google.maps.drawing.OverlayType.POLYLINE,
], ],
}, },

View File

@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

142
frontend/lib/marketData.ts Normal file
View File

@ -0,0 +1,142 @@
export interface ITrend {
direction: "up" | "down";
value: string;
}
export 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;
}
export 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,
};
});
};
export 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;
};
export 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),
}));
};

6881
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -40,12 +40,13 @@
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"next": "15.1.0", "next": "15.1.0",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"next-themes": "^0.4.4", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-day-picker": "8.10.1", "react-day-picker": "8.10.1",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"recharts": "^2.15.1", "recharts": "^2.15.1",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2" "zod": "^3.24.2"

File diff suppressed because it is too large Load Diff