Add irrigation and fertilization recommendations, farm AI assistant, and pest detection features with Persian localization

- 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.
This commit is contained in:
2026-02-21 00:19:25 +03:30
parent 0eb109725e
commit 2b6538c650
17 changed files with 2448 additions and 0 deletions
@@ -0,0 +1,542 @@
'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>
)
}