2b6538c650
- Introduced new sections in the dashboard for irrigation recommendations, fertilization recommendations, farm AI assistant, and pest detection. - Added Persian translations for new features to enhance user experience. - Updated the vertical menu to include links to the new sections. - Enhanced global styles with animations for improved UI interactions.
543 lines
19 KiB
TypeScript
543 lines
19 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import Box from '@mui/material/Box'
|
|
import Card from '@mui/material/Card'
|
|
import CardContent from '@mui/material/CardContent'
|
|
import Typography from '@mui/material/Typography'
|
|
import Button from '@mui/material/Button'
|
|
import Collapse from '@mui/material/Collapse'
|
|
|
|
// Types
|
|
interface FarmData {
|
|
soilType: string
|
|
organicMatter: string
|
|
waterEC: string
|
|
}
|
|
|
|
interface GrowthStage {
|
|
id: string
|
|
icon: string
|
|
}
|
|
|
|
interface CropOption {
|
|
id: string
|
|
labelKey: string
|
|
icon: string
|
|
}
|
|
|
|
interface FertilizationPlan {
|
|
npkRatio: string
|
|
amountPerHectare: string
|
|
applicationMethod: string
|
|
applicationInterval: string
|
|
reasoning: string
|
|
}
|
|
|
|
// Mock farm data (from stored soil/water data - no inputs)
|
|
const DEFAULT_FARM_DATA: FarmData = {
|
|
soilType: 'Loamy',
|
|
organicMatter: 'Medium (2.5%)',
|
|
waterEC: '1.2 dS/m'
|
|
}
|
|
|
|
const GROWTH_STAGES: GrowthStage[] = [
|
|
{ id: 'prePlanting', icon: 'tabler-seedling' },
|
|
{ id: 'earlyGrowth', icon: 'tabler-leaf' },
|
|
{ id: 'flowering', icon: 'tabler-flower' },
|
|
{ id: 'fruiting', icon: 'tabler-apple' },
|
|
{ id: 'postHarvest', icon: 'tabler-basket' }
|
|
]
|
|
|
|
const CROP_OPTIONS: CropOption[] = [
|
|
{ id: 'wheat', labelKey: 'wheat', icon: 'tabler-wheat' },
|
|
{ id: 'corn', labelKey: 'corn', icon: 'tabler-plant-2' },
|
|
{ id: 'cotton', labelKey: 'cotton', icon: 'tabler-flower' },
|
|
{ id: 'saffron', labelKey: 'saffron', icon: 'tabler-flower-2' },
|
|
{ id: 'canola', labelKey: 'canola', icon: 'tabler-leaf' },
|
|
{ id: 'vegetables', labelKey: 'vegetables', icon: 'tabler-carrot' }
|
|
]
|
|
|
|
// Mock plan generator (replace with API in production)
|
|
function generateFertilizationPlan(
|
|
_cropId: string,
|
|
_growthStageId: string,
|
|
_farmData: FarmData
|
|
): FertilizationPlan {
|
|
return {
|
|
npkRatio: '20-20-20 (NPK)',
|
|
amountPerHectare: '150 kg/ha',
|
|
applicationMethod: 'Foliar spray + soil broadcast',
|
|
applicationInterval: 'Every 14 days',
|
|
reasoning:
|
|
'Your loamy soil with medium organic matter (2.5%) provides good nutrient retention. Water EC of 1.2 dS/m indicates low salinity—suitable for most crops. At the flowering stage, increased phosphorus supports bloom development. We recommend a balanced NPK to maintain nitrogen for vegetative growth while boosting phosphorous for flowering.'
|
|
}
|
|
}
|
|
|
|
export default function SmartFertilizationRecommendation() {
|
|
const t = useTranslations('fertilization')
|
|
const [farmData] = useState<FarmData>(DEFAULT_FARM_DATA)
|
|
const [growthStage, setGrowthStage] = useState<string>(GROWTH_STAGES[0].id)
|
|
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
|
const [plan, setPlan] = useState<FertilizationPlan | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [reasoningExpanded, setReasoningExpanded] = useState(false)
|
|
|
|
const handleGenerate = () => {
|
|
if (!selectedCrop) return
|
|
setLoading(true)
|
|
setPlan(null)
|
|
setReasoningExpanded(false)
|
|
setTimeout(() => {
|
|
setPlan(
|
|
generateFertilizationPlan(selectedCrop, growthStage, farmData)
|
|
)
|
|
setLoading(false)
|
|
}, 1400)
|
|
}
|
|
|
|
const stageIndex = GROWTH_STAGES.findIndex(s => s.id === growthStage)
|
|
|
|
return (
|
|
<Box
|
|
className='min-bs-screen pb-28'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(165deg, #f0fdf4 0%, #ecfdf5 25%, #faf5ff 60%, var(--mui-palette-background-default) 100%)',
|
|
minHeight: '100vh'
|
|
}}
|
|
>
|
|
<Box className='max-w-lg mx-auto px-4 py-6 sm:py-8'>
|
|
{/* 1) Header */}
|
|
<Box className='mb-8'>
|
|
<Typography
|
|
variant='h4'
|
|
className='font-bold tracking-tight'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(135deg, #16a34a 0%, #22c55e 40%, #7c3aed 100%)',
|
|
backgroundClip: 'text',
|
|
WebkitBackgroundClip: 'text',
|
|
color: 'transparent',
|
|
fontSize: { xs: '1.5rem', sm: '1.75rem' }
|
|
}}
|
|
>
|
|
{t('title')}
|
|
</Typography>
|
|
<Typography
|
|
variant='body2'
|
|
color='text.secondary'
|
|
className='mt-1 transition-colors duration-300'
|
|
>
|
|
{t('subtitle')}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* 2) Farm Data Card */}
|
|
<Card
|
|
elevation={0}
|
|
className='mb-6 overflow-hidden transition-all duration-300 hover:shadow-lg animate-fade-in'
|
|
sx={{
|
|
borderRadius: '28px',
|
|
background:
|
|
'linear-gradient(145deg, #ffffff 0%, #faf5ff 50%, #f0fdf4 100%)',
|
|
boxShadow:
|
|
'0 4px 24px rgba(34, 197, 94, 0.08), 0 4px 12px rgba(124, 58, 237, 0.04), 0 1px 3px rgba(0,0,0,0.04)',
|
|
border: '1px solid rgba(34, 197, 94, 0.12)'
|
|
}}
|
|
>
|
|
<CardContent className='p-5'>
|
|
<Box className='flex items-center justify-between mbe-4'>
|
|
<Typography variant='subtitle2' fontWeight={600} color='text.secondary'>
|
|
{t('farmData.title')}
|
|
</Typography>
|
|
<Box
|
|
className='px-2.5 py-1 rounded-full text-xs font-medium flex items-center gap-1.5'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
|
|
color: 'white',
|
|
boxShadow: '0 2px 8px rgba(34, 197, 94, 0.3)'
|
|
}}
|
|
>
|
|
<i className='tabler-circle-check text-sm' />
|
|
{t('verifiedBadge')}
|
|
</Box>
|
|
</Box>
|
|
<Box className='flex flex-wrap gap-3'>
|
|
<FarmBadge icon='tabler-seedling' label={t('farmData.soilType')} value={farmData.soilType} />
|
|
<FarmBadge
|
|
icon='tabler-atom-2'
|
|
label={t('farmData.organicMatter')}
|
|
value={farmData.organicMatter}
|
|
/>
|
|
<FarmBadge icon='tabler-droplet' label={t('farmData.waterEC')} value={farmData.waterEC} />
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 3) Growth Stage Selector */}
|
|
<Typography variant='subtitle2' fontWeight={600} color='text.secondary' className='mbe-3'>
|
|
{t('growthStage.title')}
|
|
</Typography>
|
|
<Box className='flex gap-2 mb-6 overflow-x-auto pb-2 -mx-1 px-1 scrollbar-hide'>
|
|
{GROWTH_STAGES.map((stage, idx) => {
|
|
const isSelected = growthStage === stage.id
|
|
const isPast = idx < stageIndex
|
|
return (
|
|
<Box
|
|
key={stage.id}
|
|
component='button'
|
|
type='button'
|
|
onClick={() => setGrowthStage(stage.id)}
|
|
className='flex flex-col items-center gap-1.5 px-4 py-3 rounded-2xl shrink-0 transition-all duration-300 ease-out border-2 cursor-pointer min-w-[72px]'
|
|
sx={{
|
|
borderColor: isSelected ? '#22c55e' : 'transparent',
|
|
background: isSelected
|
|
? 'linear-gradient(145deg, rgba(34, 197, 94, 0.15) 0%, rgba(124, 58, 237, 0.06) 100%)'
|
|
: 'linear-gradient(145deg, #ffffff 0%, #faf5ff 100%)',
|
|
boxShadow: isSelected
|
|
? '0 4px 20px rgba(34, 197, 94, 0.2), inset 0 1px 0 rgba(255,255,255,0.8)'
|
|
: '0 2px 8px rgba(0,0,0,0.04)',
|
|
'&:hover': {
|
|
transform: 'translateY(-2px)',
|
|
boxShadow: isSelected
|
|
? '0 6px 24px rgba(34, 197, 94, 0.25)'
|
|
: '0 4px 16px rgba(124, 58, 237, 0.1)'
|
|
}
|
|
}}
|
|
>
|
|
<Box
|
|
className='w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300'
|
|
sx={{
|
|
background: isSelected
|
|
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
|
|
: isPast
|
|
? 'linear-gradient(145deg, rgba(34, 197, 94, 0.2) 0%, rgba(34, 197, 94, 0.1) 100%)'
|
|
: 'rgba(124, 58, 237, 0.08)'
|
|
}}
|
|
>
|
|
<i
|
|
className={`${stage.icon} text-xl transition-colors duration-300 ${
|
|
isSelected ? 'text-white' : isPast ? 'text-emerald-600' : 'text-violet-500'
|
|
}`}
|
|
/>
|
|
</Box>
|
|
<Typography
|
|
variant='caption'
|
|
fontWeight={600}
|
|
sx={{
|
|
color: isSelected ? '#16a34a' : 'text.secondary',
|
|
textAlign: 'center',
|
|
lineHeight: 1.2
|
|
}}
|
|
>
|
|
{t(`growthStage.${stage.id}`)}
|
|
</Typography>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
|
|
{/* 4) Plant Selection */}
|
|
<Typography variant='subtitle2' fontWeight={600} color='text.secondary' className='mbe-3'>
|
|
{t('plantSelection.title')}
|
|
</Typography>
|
|
<Box className='flex flex-wrap gap-3 mb-8'>
|
|
{CROP_OPTIONS.map(crop => (
|
|
<CropCard
|
|
key={crop.id}
|
|
crop={crop}
|
|
label={t(`crops.${crop.labelKey}`)}
|
|
selected={selectedCrop === crop.id}
|
|
onClick={() =>
|
|
setSelectedCrop(prev => (prev === crop.id ? null : crop.id))
|
|
}
|
|
/>
|
|
))}
|
|
</Box>
|
|
|
|
{/* 6) Result Section - Prescription style */}
|
|
{plan && (
|
|
<Box className='mb-6 animate-fade-in'>
|
|
<Card
|
|
elevation={0}
|
|
sx={{
|
|
borderRadius: '28px',
|
|
background:
|
|
'linear-gradient(160deg, #ffffff 0%, #faf5ff 40%, #f0fdf4 100%)',
|
|
boxShadow:
|
|
'0 8px 32px rgba(34, 197, 94, 0.12), 0 4px 16px rgba(124, 58, 237, 0.06), 0 2px 8px rgba(0,0,0,0.04)',
|
|
border: '1px solid rgba(34, 197, 94, 0.15)',
|
|
overflow: 'visible'
|
|
}}
|
|
>
|
|
<CardContent className='p-6'>
|
|
<Box className='flex items-center gap-2 mbe-5'>
|
|
<i className='tabler-prescription text-2xl text-emerald-600' />
|
|
<Typography variant='h6' fontWeight={700} color='text.primary'>
|
|
{t('result.title')}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Box className='space-y-3'>
|
|
<PrescriptionRow
|
|
icon='tabler-atom-2'
|
|
label={t('result.fertilizerType')}
|
|
value={plan.npkRatio}
|
|
/>
|
|
<PrescriptionRow
|
|
icon='tabler-scale'
|
|
label={t('result.amountPerHectare')}
|
|
value={plan.amountPerHectare}
|
|
/>
|
|
<PrescriptionRow
|
|
icon='tabler-spray'
|
|
label={t('result.applicationMethod')}
|
|
value={plan.applicationMethod}
|
|
/>
|
|
<PrescriptionRow
|
|
icon='tabler-calendar-repeat'
|
|
label={t('result.applicationInterval')}
|
|
value={plan.applicationInterval}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Expandable "Why this recommendation?" */}
|
|
<Box
|
|
className='mt-5 rounded-2xl overflow-hidden transition-all duration-300'
|
|
sx={{
|
|
border: '1px solid rgba(34, 197, 94, 0.15)',
|
|
background: 'rgba(34, 197, 94, 0.04)'
|
|
}}
|
|
>
|
|
<Box
|
|
component='button'
|
|
type='button'
|
|
onClick={() => setReasoningExpanded(!reasoningExpanded)}
|
|
className='w-full flex items-center justify-between px-4 py-3 text-start cursor-pointer'
|
|
sx={{ '&:hover': { bgcolor: 'rgba(34, 197, 94, 0.06)' } }}
|
|
>
|
|
<Box className='flex items-center gap-2'>
|
|
<i className='tabler-brain text-lg text-emerald-600' />
|
|
<Typography variant='subtitle2' fontWeight={600} color='text.primary'>
|
|
{t('result.whyRecommendation')}
|
|
</Typography>
|
|
</Box>
|
|
<i
|
|
className={`tabler-chevron-down text-xl text-emerald-600 transition-transform duration-300 ${
|
|
reasoningExpanded ? 'rotate-180' : ''
|
|
}`}
|
|
/>
|
|
</Box>
|
|
<Collapse in={reasoningExpanded}>
|
|
<Box className='px-4 pb-4'>
|
|
<Typography
|
|
variant='body2'
|
|
color='text.secondary'
|
|
sx={{ lineHeight: 1.7 }}
|
|
>
|
|
{plan.reasoning}
|
|
</Typography>
|
|
</Box>
|
|
</Collapse>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Loading state */}
|
|
{loading && (
|
|
<Card
|
|
elevation={0}
|
|
className='mb-6 animate-fade-in'
|
|
sx={{
|
|
borderRadius: '28px',
|
|
background:
|
|
'linear-gradient(160deg, #ffffff 0%, #f0fdf4 100%)',
|
|
boxShadow: '0 8px 32px rgba(34, 197, 94, 0.1)',
|
|
border: '1px solid rgba(34, 197, 94, 0.12)'
|
|
}}
|
|
>
|
|
<CardContent className='p-12 flex flex-col items-center gap-4'>
|
|
<Box
|
|
className='w-14 h-14 rounded-2xl flex items-center justify-center animate-pulse'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(124, 58, 237, 0.08) 100%)'
|
|
}}
|
|
>
|
|
<i className='tabler-sparkles text-2xl text-emerald-600' />
|
|
</Box>
|
|
<Typography variant='body2' color='text.secondary'>
|
|
{t('generating')}
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</Box>
|
|
|
|
{/* 5) Primary CTA Button - Sticky */}
|
|
<Box
|
|
className='fixed bottom-0 start-0 end-0 p-4 z-10'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(to top, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.9) 70%, transparent 100%)',
|
|
backdropFilter: 'blur(8px)'
|
|
}}
|
|
>
|
|
<Button
|
|
fullWidth
|
|
variant='contained'
|
|
disabled={!selectedCrop || loading}
|
|
onClick={handleGenerate}
|
|
startIcon={<i className='tabler-sparkles text-xl' />}
|
|
className='rounded-2xl py-3.5 text-base font-semibold shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.01] active:scale-[0.99]'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(135deg, #22c55e 0%, #16a34a 50%, #15803d 100%)',
|
|
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.4)',
|
|
'&:hover': {
|
|
background:
|
|
'linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%)',
|
|
boxShadow: '0 6px 28px rgba(34, 197, 94, 0.5)'
|
|
},
|
|
'&:disabled': {
|
|
background: 'action.disabledBackground',
|
|
color: 'action.disabled'
|
|
}
|
|
}}
|
|
>
|
|
{t('generateCta')}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
|
|
function FarmBadge({
|
|
icon,
|
|
label,
|
|
value
|
|
}: {
|
|
icon: string
|
|
label: string
|
|
value: string
|
|
}) {
|
|
return (
|
|
<Box
|
|
className='flex items-center gap-2 px-4 py-2.5 rounded-2xl transition-all duration-200 hover:scale-[1.02] hover:shadow-md'
|
|
sx={{
|
|
background:
|
|
'linear-gradient(145deg, rgba(34, 197, 94, 0.1) 0%, rgba(124, 58, 237, 0.04) 100%)',
|
|
border: '1px solid rgba(34, 197, 94, 0.15)',
|
|
boxShadow: 'inset 0 1px 2px rgba(255,255,255,0.5)'
|
|
}}
|
|
>
|
|
<i className={`${icon} text-xl text-emerald-600`} />
|
|
<Box>
|
|
<Typography variant='caption' color='text.secondary' display='block' lineHeight={1.2}>
|
|
{label}
|
|
</Typography>
|
|
<Typography variant='body2' fontWeight={600} color='text.primary'>
|
|
{value}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
function CropCard({
|
|
crop,
|
|
label,
|
|
selected,
|
|
onClick
|
|
}: {
|
|
crop: CropOption
|
|
label: string
|
|
selected: boolean
|
|
onClick: () => void
|
|
}) {
|
|
return (
|
|
<Card
|
|
component='button'
|
|
type='button'
|
|
elevation={0}
|
|
onClick={onClick}
|
|
className='flex items-center gap-3 px-4 py-3 rounded-2xl cursor-pointer transition-all duration-300 border-2 text-start'
|
|
sx={{
|
|
borderColor: selected ? '#22c55e' : 'transparent',
|
|
background: selected
|
|
? 'linear-gradient(145deg, rgba(34, 197, 94, 0.15) 0%, rgba(124, 58, 237, 0.06) 100%)'
|
|
: 'linear-gradient(145deg, #ffffff 0%, #faf5ff 100%)',
|
|
boxShadow: selected
|
|
? '0 4px 20px rgba(34, 197, 94, 0.2), inset 0 1px 0 rgba(255,255,255,0.8)'
|
|
: '0 2px 8px rgba(0,0,0,0.04), inset 0 1px 0 rgba(255,255,255,0.9)',
|
|
'&:hover': {
|
|
transform: 'translateY(-2px)',
|
|
boxShadow: selected
|
|
? '0 6px 24px rgba(34, 197, 94, 0.25)'
|
|
: '0 4px 16px rgba(34, 197, 94, 0.12)'
|
|
}
|
|
}}
|
|
>
|
|
<Box
|
|
className='w-11 h-11 rounded-xl flex items-center justify-center shrink-0 transition-all duration-300'
|
|
sx={{
|
|
background: selected
|
|
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
|
|
: 'linear-gradient(145deg, rgba(34, 197, 94, 0.1) 0%, rgba(124, 58, 237, 0.05) 100%)'
|
|
}}
|
|
>
|
|
<i
|
|
className={`${crop.icon} text-xl ${selected ? 'text-white' : 'text-emerald-600'}`}
|
|
/>
|
|
</Box>
|
|
<Typography
|
|
variant='body2'
|
|
fontWeight={600}
|
|
color={selected ? '#16a34a' : 'text.primary'}
|
|
>
|
|
{label}
|
|
</Typography>
|
|
{selected && (
|
|
<i className='tabler-circle-check-filled text-xl text-emerald-600 ms-auto' />
|
|
)}
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function PrescriptionRow({
|
|
icon,
|
|
label,
|
|
value
|
|
}: {
|
|
icon: string
|
|
label: string
|
|
value: string
|
|
}) {
|
|
return (
|
|
<Box
|
|
className='flex items-center gap-4 p-3 rounded-2xl transition-colors duration-200'
|
|
sx={{
|
|
background: 'rgba(34, 197, 94, 0.06)',
|
|
border: '1px solid rgba(34, 197, 94, 0.08)'
|
|
}}
|
|
>
|
|
<i className={`${icon} text-2xl text-emerald-600 shrink-0`} />
|
|
<Box className='flex-1 min-w-0'>
|
|
<Typography variant='caption' color='text.secondary'>
|
|
{label}
|
|
</Typography>
|
|
<Typography variant='body1' fontWeight={600} color='text.primary'>
|
|
{value}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|