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,671 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import Card from '@mui/material/Card'
import IconButton from '@mui/material/IconButton'
import TextField from '@mui/material/TextField'
import Collapse from '@mui/material/Collapse'
import classnames from 'classnames'
import type { FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes'
// ─── Constants ─────────────────────────────────────────────────────────────
const DEFAULT_FARM_CONTEXT: FarmContext = {
soilType: 'Loamy',
waterEC: '1.2 dS/m',
selectedCrop: 'Tomato',
growthStage: 'Flowering',
lastIrrigationStatus: '2 days ago'
}
const SUGGESTION_CHIPS = [
{ id: 'yellow-leaves', labelKey: 'suggestions.yellowLeaves' },
{ id: 'irrigation-plan', labelKey: 'suggestions.irrigationPlan' },
{ id: 'fertilizer-flowering', labelKey: 'suggestions.fertilizerFlowering' },
{ id: 'plant-disease', labelKey: 'suggestions.plantDisease' }
]
// Demo structured AI response for display
const DEMO_AI_RESPONSE_SECTIONS: AIResponseSection[] = [
{
type: 'recommendation',
title: 'Irrigation recommendation',
icon: 'droplet',
frequency: '3 times per week',
amount: '1520 L per plant',
timing: 'Early morning (05:0007:00)',
expandableExplanation:
'Your loamy soil holds moisture well, but tomatoes during flowering need consistent moisture. Water EC of 1.2 dS/m is suitable. Last irrigation was 2 days ago—avoid overwatering to prevent blossom-end rot.'
},
{
type: 'list',
title: 'Key points',
icon: 'leaf',
items: [
'Avoid midday watering to reduce evaporation',
'Drip irrigation preferred for root zone targeting',
'Monitor soil moisture before each session'
]
},
{
type: 'warning',
title: 'Weather advisory',
icon: 'warning',
content: 'High temps forecasted next week. Consider increasing frequency to 4x/week temporarily.'
}
]
// ─── Main Component ────────────────────────────────────────────────────────
export default function FarmAiAssistantChat() {
const t = useTranslations('farmAiAssistant')
const [messages, setMessages] = useState<FarmAIMessage[]>([])
const [inputValue, setInputValue] = useState('')
const [isContextExpanded, setIsContextExpanded] = useState(true)
const [isTyping, setIsTyping] = useState(false)
const [selectedChip, setSelectedChip] = useState<string | null>(null)
const [expandedExplanations, setExpandedExplanations] = useState<Set<string>>(new Set())
const scrollRef = useRef<HTMLDivElement>(null)
const farmContext = DEFAULT_FARM_CONTEXT
// Scroll to bottom on new messages
useEffect(() => {
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: 'smooth'
})
}, [messages, isTyping])
const handleSuggestionClick = (chipId: string, labelKey: string) => {
const label = t(labelKey)
setSelectedChip(prev => (prev === chipId ? null : chipId))
handleSend(label)
}
const handleSend = async (text?: string) => {
const content = (text || inputValue).trim()
if (!content) return
setInputValue('')
setSelectedChip(null)
const userMessage: FarmAIMessage = {
id: `u-${Date.now()}`,
role: 'user',
content,
timestamp: new Date()
}
setMessages(prev => [...prev, userMessage])
setIsTyping(true)
// Simulate AI response (replace with actual API call)
await new Promise(resolve => setTimeout(resolve, 1500))
const aiMessage: FarmAIMessage = {
id: `a-${Date.now()}`,
role: 'assistant',
content: '',
timestamp: new Date(),
sections: DEMO_AI_RESPONSE_SECTIONS
}
setMessages(prev => [...prev, aiMessage])
setIsTyping(false)
}
const toggleExplanation = (id: string) => {
setExpandedExplanations(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
return (
<Box
className='flex flex-col min-bs-screen max-w-md mx-auto'
sx={{
background: 'linear-gradient(180deg, #f8fcf8 0%, #f0f7f4 30%, #e8f4ef 100%)',
minHeight: '100dvh'
}}
>
{/* 1) Smart Header */}
<Box
className='flex items-center gap-3 px-4 pt-4 pb-3 flex-shrink-0'
sx={{
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.06) 0%, rgba(14, 165, 233, 0.06) 100%)',
borderBottom: '1px solid rgba(34, 197, 94, 0.12)'
}}
>
<Box
className='w-12 h-12 rounded-2xl flex items-center justify-center shrink-0'
sx={{
background: 'linear-gradient(145deg, #22c55e 0%, #0ea5e9 100%)',
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)'
}}
>
<i className='tabler-leaf text-2xl text-white' />
</Box>
<Box className='min-w-0 flex-1'>
<Typography
variant='h6'
className='font-bold truncate'
sx={{
background: 'linear-gradient(135deg, #16a34a 0%, #0ea5e9 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
fontSize: '1.1rem'
}}
>
{t('header.title')}
</Typography>
<Typography variant='caption' color='text.secondary' className='block truncate'>
{t('header.subtitle')}
</Typography>
</Box>
</Box>
{/* 2) Expandable Farm Context Bar */}
<Box
className='mx-4 mt-3 flex-shrink-0 rounded-2xl overflow-hidden'
sx={{
background: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.04) 100%)',
border: '1px solid rgba(34, 197, 94, 0.15)',
boxShadow: '0 2px 12px rgba(0,0,0,0.04)'
}}
>
<Box
component='button'
type='button'
onClick={() => setIsContextExpanded(!isContextExpanded)}
className='w-full flex items-center justify-between px-4 py-3 text-start'
sx={{
'&:hover': { bgcolor: 'rgba(34, 197, 94, 0.04)' },
transition: 'background 0.2s'
}}
>
<Typography variant='subtitle2' fontWeight={600} color='text.secondary'>
{t('context.title')}
</Typography>
<i
className={classnames('tabler-chevron-down text-xl text-[#22c55e] transition-transform duration-300', {
'rotate-180': isContextExpanded
})}
/>
</Box>
<Collapse in={isContextExpanded}>
<Box className='px-4 pb-4 grid grid-cols-2 gap-2'>
<ContextBadge icon='tabler-seedling' label={t('context.soilType')} value={farmContext.soilType} />
<ContextBadge icon='tabler-droplet' label={t('context.waterEC')} value={farmContext.waterEC} />
<ContextBadge icon='tabler-plant-2' label={t('context.crop')} value={farmContext.selectedCrop} />
<ContextBadge icon='tabler-flower' label={t('context.growthStage')} value={farmContext.growthStage} />
<ContextBadge
icon='tabler-calendar'
label={t('context.lastIrrigation')}
value={farmContext.lastIrrigationStatus}
colSpan={2}
/>
</Box>
</Collapse>
</Box>
{/* 3) Chat Area */}
<Box
ref={scrollRef}
className='flex-1 overflow-y-auto overflow-x-hidden px-4 py-4 space-y-4'
sx={{ scrollBehavior: 'smooth' }}
>
{messages.length === 0 && !isTyping && (
<Box className='flex flex-col items-center justify-center py-16 text-center'>
<Box
className='w-20 h-20 rounded-3xl flex items-center justify-center mb-4'
sx={{
background: 'linear-gradient(145deg, rgba(34, 197, 94, 0.1) 0%, rgba(14, 165, 233, 0.1) 100%)'
}}
>
<i className='tabler-message-circle text-4xl text-[#22c55e]' />
</Box>
<Typography variant='body1' color='text.secondary' className='mb-1'>
{t('emptyState.title')}
</Typography>
<Typography variant='caption' color='text.secondary'>
{t('emptyState.subtitle')}
</Typography>
</Box>
)}
{messages.map(msg => (
<MessageBubble
key={msg.id}
message={msg}
expandedExplanations={expandedExplanations}
onToggleExplanation={toggleExplanation}
t={t}
/>
))}
{/* Typing indicator */}
{isTyping && (
<Box className='animate-fade-in flex items-start gap-3'>
<Box
className='w-9 h-9 rounded-xl flex items-center justify-center shrink-0'
sx={{
background: 'linear-gradient(145deg, #22c55e 0%, #0ea5e9 100%)'
}}
>
<i className='tabler-leaf text-lg text-white' />
</Box>
<Box
className='px-4 py-3 rounded-2xl rounded-tl-md'
sx={{
background: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.05) 100%)',
border: '1px solid rgba(34, 197, 94, 0.15)',
boxShadow: '0 2px 12px rgba(0,0,0,0.04)'
}}
>
<TypingIndicator />
</Box>
</Box>
)}
</Box>
{/* 4) Suggestion Chips */}
<Box className='px-4 py-2 flex-shrink-0 overflow-x-auto scrollbar-hide'>
<Box className='flex gap-2 pb-2'>
{SUGGESTION_CHIPS.map(chip => (
<Box
key={chip.id}
component='button'
type='button'
onClick={() => handleSuggestionClick(chip.id, chip.labelKey)}
className={classnames(
'px-4 py-2 rounded-2xl text-sm font-medium whitespace-nowrap transition-all duration-200 shrink-0',
'hover:scale-[1.02] active:scale-[0.98]'
)}
sx={{
background:
selectedChip === chip.id
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.08) 100%)',
color: selectedChip === chip.id ? '#ffffff' : 'text.primary',
border: selectedChip === chip.id ? 'none' : '1px solid rgba(34, 197, 94, 0.2)',
boxShadow:
selectedChip === chip.id
? '0 4px 12px rgba(34, 197, 94, 0.35)'
: '0 2px 8px rgba(0,0,0,0.04)'
}}
>
{t(chip.labelKey)}
</Box>
))}
</Box>
</Box>
{/* 5) Input Area - Sticky Bottom */}
<Box
className='px-4 py-3 pb-6 flex-shrink-0'
sx={{
background: 'linear-gradient(to top, rgba(248,252,248,0.98) 0%, rgba(248,252,248,0.9) 70%, transparent 100%)',
backdropFilter: 'blur(12px)'
}}
>
<Box
className='flex items-end gap-2'
sx={{
background: '#ffffff',
borderRadius: '24px',
border: '1px solid rgba(34, 197, 94, 0.2)',
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.08)',
p: 1
}}
>
<IconButton
component='label'
htmlFor='farm-ai-camera'
size='small'
className='shrink-0'
sx={{
color: 'text.secondary',
'&:hover': { color: '#22c55e', bgcolor: 'rgba(34, 197, 94, 0.08)' }
}}
>
<i className='tabler-camera text-xl' />
<input id='farm-ai-camera' type='file' accept='image/*' hidden />
</IconButton>
<IconButton
size='small'
className='shrink-0'
sx={{
color: 'text.secondary',
'&:hover': { color: '#0ea5e9', bgcolor: 'rgba(14, 165, 233, 0.08)' }
}}
>
<i className='tabler-microphone text-xl' />
</IconButton>
<TextField
fullWidth
multiline
maxRows={4}
placeholder={t('input.placeholder')}
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}}
variant='standard'
InputProps={{
disableUnderline: true,
sx: {
px: 1.5,
py: 1,
fontSize: '0.95rem'
}
}}
sx={{ flex: 1, minWidth: 0 }}
/>
<IconButton
onClick={() => handleSend()}
disabled={!inputValue.trim()}
className='shrink-0'
sx={{
background: inputValue.trim()
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
: 'action.disabledBackground',
color: inputValue.trim() ? '#ffffff' : 'action.disabled',
'&:hover': inputValue.trim()
? {
background: 'linear-gradient(135deg, #16a34a 0%, #15803d 100%)',
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.4)'
}
: {},
'&.Mui-disabled': { background: 'action.disabledBackground', color: 'action.disabled' }
}}
>
<i className='tabler-send text-xl' />
</IconButton>
</Box>
</Box>
</Box>
)
}
// ─── Sub-components ────────────────────────────────────────────────────────
function ContextBadge({
icon,
label,
value,
colSpan = 1
}: {
icon: string
label: string
value: string
colSpan?: number
}) {
return (
<Box
className={classnames('flex items-center gap-2 px-3 py-2 rounded-xl', colSpan === 2 ? 'col-span-2' : '')}
sx={{
background: 'rgba(34, 197, 94, 0.06)',
border: '1px solid rgba(34, 197, 94, 0.12)'
}}
>
<i className={`${icon} text-lg text-[#22c55e] shrink-0`} />
<Box className='min-w-0'>
<Typography variant='caption' color='text.secondary' display='block' lineHeight={1.2}>
{label}
</Typography>
<Typography variant='body2' fontWeight={600} color='text.primary' noWrap>
{value}
</Typography>
</Box>
</Box>
)
}
function MessageBubble({
message,
expandedExplanations,
onToggleExplanation,
t
}: {
message: FarmAIMessage
expandedExplanations: Set<string>
onToggleExplanation: (id: string) => void
t: (key: string) => string
}) {
const isUser = message.role === 'user'
if (isUser) {
return (
<Box className='flex justify-end animate-fade-in'>
<Box
className='max-w-[85%] px-4 py-2.5 rounded-2xl rounded-tr-md'
sx={{
background: 'linear-gradient(145deg, rgba(34, 197, 94, 0.15) 0%, rgba(34, 197, 94, 0.08) 100%)',
border: '1px solid rgba(34, 197, 94, 0.2)'
}}
>
<Typography variant='body2' color='text.primary'>
{message.content}
</Typography>
</Box>
</Box>
)
}
// AI message - structured cards
return (
<Box className='flex items-start gap-3 animate-fade-in'>
<Box
className='w-9 h-9 rounded-xl flex items-center justify-center shrink-0'
sx={{
background: 'linear-gradient(145deg, #22c55e 0%, #0ea5e9 100%)'
}}
>
<i className='tabler-leaf text-lg text-white' />
</Box>
<Box className='flex-1 min-w-0 space-y-3'>
{message.sections?.map((section, idx) => (
<AISectionCard
key={`${message.id}-${idx}`}
section={section}
expandedExplanations={expandedExplanations}
onToggleExplanation={onToggleExplanation}
messageId={message.id}
idx={idx}
t={t}
/>
))}
</Box>
</Box>
)
}
function AISectionCard({
section,
expandedExplanations,
onToggleExplanation,
messageId,
idx,
t
}: {
section: AIResponseSection
expandedExplanations: Set<string>
onToggleExplanation: (id: string) => void
messageId: string
idx: number
t: (key: string) => string
}) {
const expId = `${messageId}-exp-${idx}`
const iconMap = {
droplet: 'tabler-droplet',
leaf: 'tabler-leaf',
warning: 'tabler-alert-triangle',
fertilizer: 'tabler-atom-2',
calendar: 'tabler-calendar'
}
const iconClass = section.icon ? iconMap[section.icon] : 'tabler-leaf'
if (section.type === 'recommendation') {
return (
<Card
elevation={0}
className='overflow-hidden'
sx={{
borderRadius: '20px',
background: 'linear-gradient(160deg, #ffffff 0%, rgba(34, 197, 94, 0.06) 100%)',
border: '1px solid rgba(34, 197, 94, 0.2)',
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.12)'
}}
>
<Box className='p-4'>
<Box className='flex items-center gap-2 mb-3'>
<i className={`${iconClass} text-xl text-[#22c55e]`} />
<Typography variant='subtitle2' fontWeight={700} color='primary'>
{section.title}
</Typography>
</Box>
<Box className='space-y-2'>
{section.frequency && (
<Box className='flex justify-between'>
<Typography variant='caption' color='text.secondary'>
{t('recommendation.frequency')}
</Typography>
<Typography variant='body2' fontWeight={600}>
{section.frequency}
</Typography>
</Box>
)}
{section.amount && (
<Box className='flex justify-between'>
<Typography variant='caption' color='text.secondary'>
{t('recommendation.amount')}
</Typography>
<Typography variant='body2' fontWeight={600}>
{section.amount}
</Typography>
</Box>
)}
{section.timing && (
<Box className='flex justify-between'>
<Typography variant='caption' color='text.secondary'>
{t('recommendation.timing')}
</Typography>
<Typography variant='body2' fontWeight={600}>
{section.timing}
</Typography>
</Box>
)}
</Box>
{section.expandableExplanation && (
<Box className='mt-3'>
<Box
component='button'
type='button'
onClick={() => onToggleExplanation(expId)}
className='flex items-center gap-1 text-sm font-medium'
sx={{ color: '#22c55e', '&:hover': { color: '#16a34a' } }}
>
{t('recommendation.whyThis')}
<i
className={classnames('tabler-chevron-down text-base transition-transform', {
'rotate-180': expandedExplanations.has(expId)
})}
/>
</Box>
<Collapse in={expandedExplanations.has(expId)}>
<Typography variant='body2' color='text.secondary' className='mt-2'>
{section.expandableExplanation}
</Typography>
</Collapse>
</Box>
)}
</Box>
</Card>
)
}
if (section.type === 'warning') {
return (
<Box
className='p-4 rounded-2xl'
sx={{
background: 'linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, rgba(245, 158, 11, 0.08) 100%)',
border: '1px solid rgba(251, 191, 36, 0.35)'
}}
>
<Box className='flex gap-2'>
<i className='tabler-alert-triangle text-xl text-amber-600 shrink-0 mt-0.5' />
<Box>
{section.title && (
<Typography variant='subtitle2' fontWeight={600} color='warning.dark' className='mb-1'>
{section.title}
</Typography>
)}
<Typography variant='body2' color='text.secondary'>
{section.content}
</Typography>
</Box>
</Box>
</Box>
)
}
if (section.type === 'list') {
return (
<Box
className='p-4 rounded-2xl'
sx={{
background: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.04) 100%)',
border: '1px solid rgba(34, 197, 94, 0.15)'
}}
>
{section.title && (
<Box className='flex items-center gap-2 mb-2'>
<i className={`${iconClass} text-lg text-[#22c55e]`} />
<Typography variant='subtitle2' fontWeight={600} color='text.primary'>
{section.title}
</Typography>
</Box>
)}
{section.items && (
<Box component='ul' className='m-0 ps-5 space-y-1'>
{section.items.map((item, i) => (
<Typography key={i} component='li' variant='body2' color='text.secondary'>
{item}
</Typography>
))}
</Box>
)}
</Box>
)
}
return null
}
function TypingIndicator() {
return (
<Box className='flex gap-1'>
{[0, 1, 2].map(i => (
<Box
key={i}
className='w-2 h-2 rounded-full bg-[#22c55e]'
sx={{
animation: `typing-bounce 1.4s ease-in-out ${i * 0.16}s infinite both`
}}
/>
))}
</Box>
)
}
@@ -0,0 +1,36 @@
export interface FarmContext {
soilType: string
waterEC: string
selectedCrop: string
growthStage: string
lastIrrigationStatus: string
}
export interface SuggestionChip {
id: string
label: string
}
// Structured AI response sections for card-based rendering
export interface AIResponseSection {
type: 'text' | 'list' | 'recommendation' | 'warning'
title?: string
content?: string
items?: string[]
icon?: 'droplet' | 'leaf' | 'warning' | 'fertilizer' | 'calendar'
// Recommendation-specific
frequency?: string
amount?: string
timing?: string
expandableExplanation?: string
}
export interface FarmAIMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: Date
images?: string[]
// For structured AI responses
sections?: AIResponseSection[]
}
@@ -0,0 +1,2 @@
export { default as FarmAiAssistantChat } from './FarmAiAssistantChat'
export * from './farmAiAssistantTypes'
@@ -0,0 +1,193 @@
'use client'
import { useState, useCallback } 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 CircularProgress from '@mui/material/CircularProgress'
import Collapse from '@mui/material/Collapse'
import UploadBox from './components/UploadBox'
import ResultCard from './components/ResultCard'
import type { UploadedFile } from './components/UploadBox'
import type { PestResult } from './components/ResultCard'
export default function PlantPestDetection() {
const t = useTranslations('pestDetection')
const [file, setFile] = useState<UploadedFile | null>(null)
const [result, setResult] = useState<PestResult | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleFileSelect = useCallback((newFile: UploadedFile | null) => {
setFile(newFile)
setResult(null)
setError(null)
}, [])
const handleAnalyze = useCallback(() => {
if (!file) return
setLoading(true)
setError(null)
setResult(null)
const delay = 1500 + Math.random() * 1000
setTimeout(() => {
setResult({
pest: t('mockResult.pest'),
confidence: 92,
description: t('mockResult.description'),
treatment: t('mockResult.treatment'),
})
setLoading(false)
}, delay)
}, [file, t])
const handleReset = useCallback(() => {
if (file) {
URL.revokeObjectURL(file.preview)
}
setFile(null)
setResult(null)
setError(null)
setLoading(false)
}, [file])
return (
<Box
className="min-bs-screen pb-24"
sx={{
minHeight: '100vh',
}}
>
<Box className="max-w-2xl mx-auto px-4 py-6 sm:py-8">
{/* Header */}
<Box className="mb-8">
<Typography
variant="h4"
className="font-bold tracking-tight"
sx={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 50%, #15803d 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">
{t('subtitle')}
</Typography>
</Box>
{/* Upload card */}
<Card
elevation={0}
className="mb-6 overflow-hidden transition-all duration-300 hover:shadow-lg"
sx={{
borderRadius: '24px',
background: 'linear-gradient(145deg, #ffffff 0%, #f8fcf8 100%)',
boxShadow: '0 4px 24px rgba(34, 197, 94, 0.08), 0 1px 3px rgba(0,0,0,0.04)',
border: '1px solid rgba(34, 197, 94, 0.12)',
}}
>
<CardContent className="p-5 sm:p-6">
<UploadBox
file={file}
onFileSelect={handleFileSelect}
onError={setError}
error={error ?? undefined}
/>
{/* Action buttons */}
<Box className="mt-6 flex flex-col gap-3 sm:flex-row sm:justify-center">
<Button
variant="contained"
disabled={!file || loading}
onClick={handleAnalyze}
startIcon={
loading ? (
<CircularProgress size={20} color="inherit" sx={{ color: 'white' }} />
) : (
<i className="tabler-scan text-xl" />
)
}
className="rounded-xl py-3 px-8 font-semibold shadow-md transition-all duration-300 hover:shadow-lg hover:scale-[1.02] active:scale-[0.98]"
sx={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 50%, #15803d 100%)',
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.35)',
'&:hover': {
background: 'linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%)',
boxShadow: '0 6px 24px rgba(34, 197, 94, 0.45)',
},
'&:disabled': {
background: 'action.disabledBackground',
color: 'action.disabled',
},
}}
>
{loading ? t('analyzing') : t('analyze')}
</Button>
{file && (
<Button
variant="outlined"
onClick={handleReset}
disabled={loading}
startIcon={<i className="tabler-rotate-2 text-lg" />}
className="rounded-xl py-3 px-8 font-semibold"
sx={{
borderColor: 'divider',
'&:hover': {
borderColor: 'primary.main',
backgroundColor: 'action.hover',
},
}}
>
{t('reset')}
</Button>
)}
</Box>
</CardContent>
</Card>
{/* Loading state */}
<Collapse in={loading}>
<Card
elevation={0}
className="mb-6"
sx={{
borderRadius: '24px',
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">
<CircularProgress size={48} sx={{ color: '#22c55e' }} />
<Typography variant="body2" color="text.secondary">
{t('analyzing')}
</Typography>
</CardContent>
</Card>
</Collapse>
{/* Result card */}
<Collapse in={!!result && !loading}>
{result && !loading && (
<Box className="mb-6">
<Typography variant="subtitle2" fontWeight={600} color="text.secondary" className="mbe-3">
{t('resultTitle')}
</Typography>
<ResultCard result={result} />
</Box>
)}
</Collapse>
</Box>
</Box>
)
}
@@ -0,0 +1,118 @@
'use client'
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 LinearProgress from '@mui/material/LinearProgress'
import CustomAvatar from '@core/components/mui/Avatar'
export interface PestResult {
pest: string
confidence: number
description: string
treatment: string
}
interface ResultCardProps {
result: PestResult
}
function getConfidenceColor(confidence: number): 'success' | 'warning' | 'error' {
if (confidence > 80) return 'success'
if (confidence >= 50) return 'warning'
return 'error'
}
export default function ResultCard({ result }: ResultCardProps) {
const t = useTranslations('pestDetection.resultCard')
const color = getConfidenceColor(result.confidence)
return (
<Card
elevation={0}
sx={{
borderRadius: '24px',
overflow: 'hidden',
border: '1px solid',
borderColor: `${color}.main`,
background: (theme) =>
`linear-gradient(160deg, ${theme.palette.background.paper} 0%, ${theme.palette[color].lighterOpacity} 100%)`,
boxShadow: '0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04)',
}}
>
<CardContent className="p-5 sm:p-6">
{/* Pest name & icon */}
<Box className="flex items-center gap-3 mbe-4">
<CustomAvatar
variant="rounded"
skin="filled"
color={color}
sx={{ width: 48, height: 48 }}
>
<i className="tabler-bug text-2xl" />
</CustomAvatar>
<Box>
<Typography variant="h6" fontWeight={700} color="text.primary">
{result.pest}
</Typography>
<Typography variant="caption" color="text.secondary">
{t('detectedPest')}
</Typography>
</Box>
</Box>
{/* Confidence bar */}
<Box className="mbe-5">
<Box className="flex justify-between mbe-2">
<Typography variant="caption" fontWeight={600} color="text.secondary">
{t('confidence')}
</Typography>
<Typography variant="body2" fontWeight={700} color={color}>
{result.confidence}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={result.confidence}
color={color}
sx={{
height: 10,
borderRadius: 5,
'& .MuiLinearProgress-bar': {
borderRadius: 5,
},
}}
/>
</Box>
{/* Description */}
<Box className="mbe-5">
<Box className="flex items-center gap-2 mbe-2">
<i className="tabler-leaf text-lg text-success" />
<Typography variant="caption" fontWeight={600} color="text.secondary">
{t('description')}
</Typography>
</Box>
<Typography variant="body2" color="text.primary">
{result.description}
</Typography>
</Box>
{/* Treatment */}
<Box>
<Box className="flex items-center gap-2 mbe-2">
<i className="tabler-heart-rate-monitor text-lg text-primary" />
<Typography variant="caption" fontWeight={600} color="text.secondary">
{t('recommendedTreatment')}
</Typography>
</Box>
<Typography variant="body2" color="text.primary">
{result.treatment}
</Typography>
</Box>
</CardContent>
</Card>
)
}
@@ -0,0 +1,176 @@
'use client'
import { useDropzone } from 'react-dropzone'
import { useTranslations } from 'next-intl'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import Button from '@mui/material/Button'
import { styled } from '@mui/material/styles'
import type { BoxProps } from '@mui/material/Box'
import CustomAvatar from '@core/components/mui/Avatar'
import AppReactDropzone from '@/libs/styles/AppReactDropzone'
export interface UploadedFile {
file: File
preview: string
}
interface UploadBoxProps {
file: UploadedFile | null
onFileSelect: (file: UploadedFile | null) => void
onError?: (message: string | null) => void
error?: string
}
const DropzoneWrapper = styled(AppReactDropzone)<BoxProps>(({ theme }) => ({
'& .dropzone': {
minHeight: 200,
padding: theme.spacing(4, 3),
borderRadius: '16px',
border: '2px dashed',
borderColor: 'var(--mui-palette-divider)',
transition: 'all 0.3s ease',
'&:hover': {
borderColor: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-lighterOpacity)',
},
},
'& .dropzone.active': {
borderColor: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-lighterOpacity)',
},
'& .dropzone.hasFile': {
borderColor: 'var(--mui-palette-primary-main)',
minHeight: 'unset',
padding: theme.spacing(3),
},
'& .dropzone.error': {
borderColor: 'var(--mui-palette-error-main)',
backgroundColor: 'var(--mui-palette-error-lighterOpacity)',
},
}))
export default function UploadBox({ file, onFileSelect, onError, error }: UploadBoxProps) {
const t = useTranslations('pestDetection.upload')
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop: (acceptedFiles) => {
onError?.(null)
const validFile = acceptedFiles[0]
if (validFile) {
onFileSelect({
file: validFile,
preview: URL.createObjectURL(validFile),
})
} else {
onFileSelect(null)
}
},
onDropRejected: () => {
onError?.(t('invalidFile'))
onFileSelect(null)
},
accept: {
'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'],
},
maxFiles: 1,
maxSize: 10 * 1024 * 1024, // 10MB
})
const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation()
if (file) {
URL.revokeObjectURL(file.preview)
onFileSelect(null)
}
}
const dropzoneClass = [
'dropzone',
isDragActive && 'active',
file && 'hasFile',
error && 'error',
]
.filter(Boolean)
.join(' ')
return (
<DropzoneWrapper>
<div {...getRootProps({ className: dropzoneClass })}>
<input {...getInputProps()} aria-label={t('ariaLabel')} />
{file ? (
<Box className="flex flex-col items-center gap-3 sm:flex-row sm:items-start sm:gap-4">
<Box
className="relative shrink-0 overflow-hidden rounded-xl"
sx={{
width: { xs: 140, sm: 160 },
height: { xs: 140, sm: 160 },
borderRadius: '12px',
border: '1px solid',
borderColor: 'divider',
bgcolor: 'action.hover',
}}
>
<Box
component="img"
src={file.preview}
alt={file.file.name}
sx={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
</Box>
<Box className="flex flex-1 flex-col items-center sm:items-start text-center sm:text-start min-w-0">
<Typography variant="body2" fontWeight={600} color="text.primary" noWrap sx={{ maxWidth: '100%' }}>
{file.file.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{(file.file.size / 1024).toFixed(1)} KB
</Typography>
<Button
variant="tonal"
color="error"
size="small"
onClick={handleRemove}
startIcon={<i className="tabler-trash text-base" />}
className="mt-2"
sx={{ alignSelf: { sm: 'flex-start' } }}
>
{t('remove')}
</Button>
</Box>
</Box>
) : (
<Box className="flex flex-col items-center gap-3 text-center">
<CustomAvatar
variant="rounded"
skin="light"
color="success"
className="w-16 h-16"
sx={{ width: 64, height: 64 }}
>
<i className={`${isDragActive ? 'tabler-photo-down' : 'tabler-upload'} text-3xl`} />
</CustomAvatar>
<Box>
<Typography variant="body1" fontWeight={600} color="text.primary">
{isDragActive ? t('dropHere') : t('dragDrop')}
</Typography>
<Typography variant="caption" color="text.secondary" className="mt-1 block">
{t('fileFormats')}
</Typography>
</Box>
<Button variant="tonal" size="small" color="success">
{t('dragDrop')}
</Button>
</Box>
)}
</div>
{error && (
<Typography variant="caption" color="error" className="mt-2 block" role="alert">
{error}
</Typography>
)}
</DropzoneWrapper>
)
}
@@ -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>
)
}
@@ -0,0 +1,436 @@
'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 IconButton from '@mui/material/IconButton'
import CircularProgress from '@mui/material/CircularProgress'
// Types
interface FarmInfo {
soilType: string
waterQuality: string
climateZone: string
}
interface CropOption {
id: string
labelKey: string
icon: string
}
interface IrrigationPlan {
frequencyPerWeek: number
durationMinutes: number
bestTimeOfDay: string
moistureLevel: number // 0-100
warning?: string
}
// Mock farm data (replace with API/store in production)
const DEFAULT_FARM_INFO: FarmInfo = {
soilType: 'Loamy',
waterQuality: 'Medium EC',
climateZone: 'Temperate'
}
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 generateIrrigationPlan(_cropId: string, _farmInfo: FarmInfo): IrrigationPlan {
return {
frequencyPerWeek: 4,
durationMinutes: 45,
bestTimeOfDay: '05:00 - 07:00',
moistureLevel: 72,
warning: 'Avoid irrigation during midday hours in the coming week due to forecasted high temperatures.'
}
}
export default function SmartIrrigationRecommendation() {
const t = useTranslations('irrigation')
const [farmInfo] = useState<FarmInfo>(DEFAULT_FARM_INFO)
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
const [plan, setPlan] = useState<IrrigationPlan | null>(null)
const [loading, setLoading] = useState(false)
const handleGenerate = () => {
if (!selectedCrop) return
setLoading(true)
setPlan(null)
// Simulate API delay
setTimeout(() => {
setPlan(generateIrrigationPlan(selectedCrop, farmInfo))
setLoading(false)
}, 1200)
}
return (
<Box
className='min-bs-screen pb-24'
sx={{
background: 'linear-gradient(165deg, #e0f2fe 0%, #f0f9ff 35%, #f8fcff 70%, var(--mui-palette-background-default) 100%)',
minHeight: '100vh'
}}
>
<Box className='max-w-lg mx-auto px-4 py-6 sm:py-8'>
{/* 1) Dynamic Header */}
<Box className='mb-8'>
<Typography
variant='h4'
className='font-bold tracking-tight'
sx={{
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 50%, #0369a1 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'>
{t('subtitle')}
</Typography>
</Box>
{/* 2) Farm Info Card */}
<Card
elevation={0}
className='mb-6 overflow-hidden transition-all duration-300 hover:shadow-lg'
sx={{
borderRadius: '24px',
background: 'linear-gradient(145deg, #ffffff 0%, #f8fcff 100%)',
boxShadow: '0 4px 24px rgba(14, 165, 233, 0.08), 0 1px 3px rgba(0,0,0,0.04)',
border: '1px solid rgba(14, 165, 233, 0.12)'
}}
>
<CardContent className='p-5'>
<Box className='flex items-center justify-between mbe-4'>
<Typography variant='subtitle2' fontWeight={600} color='text.secondary'>
{t('farmInfo.title')}
</Typography>
<Box className='flex items-center gap-2'>
<Box
className='px-2.5 py-1 rounded-full text-xs font-medium'
sx={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
color: 'white',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
<i className='tabler-circle-check text-sm' />
{t('verifiedBadge')}
</Box>
<IconButton size='small' sx={{ color: 'text.secondary' }} aria-label={t('editFarmInfo')}>
<i className='tabler-pencil text-lg' />
</IconButton>
</Box>
</Box>
<Box className='flex flex-wrap gap-3'>
<FarmBadge icon='tabler-seedling' label={t('farmInfo.soilType')} value={farmInfo.soilType} />
<FarmBadge icon='tabler-droplet' label={t('farmInfo.waterQuality')} value={farmInfo.waterQuality} />
<FarmBadge icon='tabler-temperature' label={t('farmInfo.climateZone')} value={farmInfo.climateZone} />
</Box>
</CardContent>
</Card>
{/* 3) Plant Selection Section */}
<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>
{/* 5) Result Card (after click) */}
{plan && (
<Box className='mb-6 animate-fade-in'>
<Card
elevation={0}
sx={{
borderRadius: '24px',
background: 'linear-gradient(160deg, #ffffff 0%, #f0f9ff 100%)',
boxShadow: '0 8px 32px rgba(14, 165, 233, 0.15), 0 2px 8px rgba(0,0,0,0.06)',
border: '1px solid rgba(14, 165, 233, 0.18)',
overflow: 'visible'
}}
>
<CardContent className='p-6'>
{/* Circular moisture indicator */}
<Box className='flex justify-center mbe-6'>
<Box className='relative'>
<svg width={120} height={120} className='-rotate-90'>
<circle
cx={60}
cy={60}
r={52}
fill='none'
stroke='rgba(14, 165, 233, 0.12)'
strokeWidth={10}
/>
<circle
cx={60}
cy={60}
r={52}
fill='none'
stroke='url(#moistureGradient)'
strokeWidth={10}
strokeLinecap='round'
strokeDasharray={`${(plan.moistureLevel / 100) * 327} 327`}
className='transition-all duration-1000 ease-out'
/>
<defs>
<linearGradient id='moistureGradient' x1='0%' y1='0%' x2='100%' y2='100%'>
<stop offset='0%' stopColor='#0ea5e9' />
<stop offset='100%' stopColor='#0284c7' />
</linearGradient>
</defs>
</svg>
<Box
className='absolute inset-0 flex flex-col items-center justify-center'
sx={{ top: 0, left: 0, right: 0, bottom: 0 }}
>
<i className='tabler-droplet text-3xl text-[#0ea5e9] mbe-0.5' />
<Typography variant='h4' fontWeight={700} color='primary.main'>
{plan.moistureLevel}%
</Typography>
<Typography variant='caption' color='text.secondary'>
{t('result.moistureLevel')}
</Typography>
</Box>
</Box>
</Box>
<Box className='space-y-4'>
<ResultRow
icon='tabler-calendar-week'
label={t('result.frequency')}
value={`${plan.frequencyPerWeek} ${t('result.timesPerWeek')}`}
/>
<ResultRow
icon='tabler-clock'
label={t('result.duration')}
value={`${plan.durationMinutes} ${t('result.minutes')}`}
/>
<ResultRow
icon='tabler-sunrise'
label={t('result.bestTime')}
value={plan.bestTimeOfDay}
/>
</Box>
{plan.warning && (
<Box
className='mt-4 p-4 rounded-2xl'
sx={{
background: 'linear-gradient(135deg, rgba(251, 191, 36, 0.12) 0%, rgba(245, 158, 11, 0.08) 100%)',
border: '1px solid rgba(251, 191, 36, 0.35)'
}}
>
<Box className='flex gap-2'>
<i className='tabler-alert-triangle text-xl text-amber-600 mt-0.5 shrink-0' />
<Box>
<Typography variant='subtitle2' fontWeight={600} color='warning.dark' className='mbe-1'>
{t('result.smartWarning')}
</Typography>
<Typography variant='body2' color='text.secondary'>
{plan.warning}
</Typography>
</Box>
</Box>
</Box>
)}
</CardContent>
</Card>
</Box>
)}
{/* Loading state */}
{loading && (
<Card
elevation={0}
className='mb-6'
sx={{
borderRadius: '24px',
background: 'linear-gradient(160deg, #ffffff 0%, #f0f9ff 100%)',
boxShadow: '0 8px 32px rgba(14, 165, 233, 0.1)',
border: '1px solid rgba(14, 165, 233, 0.12)'
}}
>
<CardContent className='p-12 flex flex-col items-center gap-4'>
<CircularProgress size={48} sx={{ color: '#0ea5e9' }} />
<Typography variant='body2' color='text.secondary'>
{t('generating')}
</Typography>
</CardContent>
</Card>
)}
</Box>
{/* 4) 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, #0ea5e9 0%, #0284c7 50%, #0369a1 100%)',
boxShadow: '0 4px 20px rgba(14, 165, 233, 0.4)',
'&:hover': {
background: 'linear-gradient(135deg, #38bdf8 0%, #0ea5e9 50%, #0284c7 100%)',
boxShadow: '0 6px 28px rgba(14, 165, 233, 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-transform duration-200 hover:scale-[1.02]'
sx={{
background: 'linear-gradient(145deg, rgba(14, 165, 233, 0.08) 0%, rgba(14, 165, 233, 0.04) 100%)',
border: '1px solid rgba(14, 165, 233, 0.15)',
boxShadow: 'inset 0 1px 2px rgba(255,255,255,0.5)'
}}
>
<i className={`${icon} text-xl text-sky-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 ? '#0ea5e9' : 'transparent',
background: selected
? 'linear-gradient(145deg, rgba(14, 165, 233, 0.12) 0%, rgba(14, 165, 233, 0.06) 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f8fcff 100%)',
boxShadow: selected
? '0 4px 20px rgba(14, 165, 233, 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(14, 165, 233, 0.25)'
: '0 4px 16px rgba(14, 165, 233, 0.12)'
}
}}
>
<Box
className='w-11 h-11 rounded-xl flex items-center justify-center shrink-0'
sx={{
background: selected
? 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)'
: 'linear-gradient(145deg, rgba(14, 165, 233, 0.1) 0%, rgba(14, 165, 233, 0.05) 100%)'
}}
>
<i className={`${crop.icon} text-xl ${selected ? 'text-white' : 'text-sky-600'}`} />
</Box>
<Typography variant='body2' fontWeight={600} color={selected ? 'primary.main' : 'text.primary'}>
{label}
</Typography>
{selected && (
<i className='tabler-circle-check-filled text-xl text-sky-600 ms-auto' />
)}
</Card>
)
}
function ResultRow({
icon,
label,
value
}: {
icon: string
label: string
value: string
}) {
return (
<Box className='flex items-center gap-4 p-3 rounded-2xl' sx={{ bgcolor: 'rgba(14, 165, 233, 0.06)' }}>
<i className={`${icon} text-2xl text-sky-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>
)
}