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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user