Add plant simulator feature with Persian localization and UI enhancements

- Introduced a new Plant Simulator section in the dashboard with a dedicated menu item.
- Added Persian translations for plant growth metrics and chart titles.
- Enhanced the GrowthChart component to utilize localized labels for better user experience.
- Updated PlantSimulator component to display plant statistics and growth visualization.
- Improved UI layout with Material-UI components for a more polished appearance.
This commit is contained in:
2026-02-20 23:48:14 +03:30
parent f9bb6a3984
commit 0eb109725e
3 changed files with 305 additions and 242 deletions
+31
View File
@@ -37,6 +37,7 @@
"waterData": "دیتاهای آب",
"soilData": "اطلاعات خاک",
"cropZoning": "زون‌بندی کشت",
"plantSimulator": "شبیه‌ساز رشد گیاه",
"dataSection": "بخش داده‌ها",
"crm": "مدیریت ارتباط با مشتری",
"analytics": "تحلیل‌ها",
@@ -561,5 +562,35 @@
"forecastChart": "پیش‌بینی هوا",
"forecastSubheader": "دما و رطوبت در ۷ روز آینده"
}
},
"plantSimulator": {
"title": "شبیه‌ساز رشد گیاه",
"height": "ارتفاع",
"leaves": "برگ",
"branches": "شاخه",
"fruits": "میوه",
"yield": "محصول (g)",
"yieldRate": "سرعت (g/s)",
"maxGrowthReached": "گیاه به حداکثر رشد رسید!",
"controls": "کنترل‌ها",
"stop": "توقف",
"start": "شروع",
"reset": "ریست",
"growthSpeed": "سرعت رشد",
"light": "نور",
"water": "آب",
"effectiveRate": "نرخ رشد مؤثر",
"chartTitle": "نمودار رشد گیاه",
"chartHeight": "ارتفاع",
"chartLeaves": "برگ",
"chartHeightPx": "ارتفاع (px)",
"chartLeafCount": "تعداد برگ",
"chartYield": "محصول (g)",
"chartYieldRate": "سرعت محصول (g/s)",
"progressGrowth": "پیشرفت رشد",
"lightStatus": "وضعیت نور",
"waterStatus": "وضعیت آب",
"yieldStatus": "محصول دهی",
"description": "این شبیه‌ساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی محاسبه می‌کند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی در باد نمایش داده می‌شود. محصول‌دهی (g) پس از ۲۰٪ رشد شروع شده و با تعداد برگ، نور و آب شتاب می‌گیرد. سرعت محصول (g/s) نشان‌دهنده نرخ لحظه‌ای تولید است. نمودار تغییرات همه شاخص‌ها را در طول زمان ثبت می‌کند."
}
}
@@ -105,6 +105,9 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
<MenuItem href="/dashboard/crop-zoning" icon={<i className="tabler-map-2" />}>
{t('cropZoning')}
</MenuItem>
<MenuItem href="/dashboard/plant-simulator" icon={<i className="tabler-flower" />}>
{t('plantSimulator')}
</MenuItem>
</MenuSection>
</Menu>
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useRef, useState, useCallback, memo } from 'react'
import { useTranslations } from 'next-intl'
import {
Chart as ChartJS,
LineElement,
@@ -13,6 +14,12 @@ import {
Filler
} from 'chart.js'
import { Line } from 'react-chartjs-2'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box'
import Grid from '@mui/material/Grid2'
import Button from '@mui/material/Button'
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Title, Tooltip, Legend, Filler)
@@ -393,40 +400,48 @@ function PlantSVG({ plant, tick, running }: { plant: PlantState; tick: number; r
// ─── Growth Chart ─────────────────────────────────────────────────────────────
const CHART_OPTIONS = {
const GrowthChart = memo(function GrowthChart({
heightHistory,
leafHistory,
yieldHistory,
yieldRateHistory
}: {
heightHistory: number[]
leafHistory: number[]
yieldHistory: number[]
yieldRateHistory: number[]
}) {
const t = useTranslations('plantSimulator')
const chartOptions = {
responsive: true,
animation: { duration: 0 },
plugins: {
legend: { labels: { color: '#e2e8f0', font: { size: 11 } } },
legend: { labels: { font: { size: 11 } } },
title: {
display: true,
text: 'نمودار رشد گیاه',
color: '#e2e8f0',
text: t('chartTitle'),
font: { size: 14 }
}
},
scales: {
x: {
ticks: { color: '#94a3b8', maxTicksLimit: 8 },
grid: { color: 'rgba(148,163,184,0.1)' }
ticks: { maxTicksLimit: 8 }
},
yHeight: {
type: 'linear' as const,
position: 'left' as const,
min: 0,
max: MAX_HEIGHT,
ticks: { color: '#4a7c59' },
grid: { color: 'rgba(148,163,184,0.1)' },
title: { display: true, text: 'ارتفاع', color: '#4a7c59' }
title: { display: true, text: t('chartHeight') }
},
yLeaf: {
type: 'linear' as const,
position: 'right' as const,
min: 0,
max: MAX_LEAVES,
ticks: { color: '#f9c74f' },
grid: { display: false },
title: { display: true, text: 'برگ', color: '#f9c74f' }
title: { display: true, text: t('chartLeaves') }
},
yYield: {
type: 'linear' as const,
@@ -446,24 +461,13 @@ const CHART_OPTIONS = {
}
}
const GrowthChart = memo(function GrowthChart({
heightHistory,
leafHistory,
yieldHistory,
yieldRateHistory
}: {
heightHistory: number[]
leafHistory: number[]
yieldHistory: number[]
yieldRateHistory: number[]
}) {
const labels = heightHistory.map((_, i) => `${i}s`)
const data = {
labels,
datasets: [
{
label: 'ارتفاع (px)',
label: t('chartHeightPx'),
data: heightHistory,
borderColor: '#4a7c59',
backgroundColor: 'rgba(74,124,89,0.10)',
@@ -473,7 +477,7 @@ const GrowthChart = memo(function GrowthChart({
yAxisID: 'yHeight'
},
{
label: 'تعداد برگ',
label: t('chartLeafCount'),
data: leafHistory,
borderColor: '#f9c74f',
backgroundColor: 'rgba(249,199,79,0.10)',
@@ -483,7 +487,7 @@ const GrowthChart = memo(function GrowthChart({
yAxisID: 'yLeaf'
},
{
label: 'محصول (g)',
label: t('chartYield'),
data: yieldHistory,
borderColor: '#f97316',
backgroundColor: 'rgba(249,115,22,0.10)',
@@ -493,7 +497,7 @@ const GrowthChart = memo(function GrowthChart({
yAxisID: 'yYield'
},
{
label: 'سرعت محصول (g/s)',
label: t('chartYieldRate'),
data: yieldRateHistory,
borderColor: '#a78bfa',
backgroundColor: 'rgba(167,139,250,0.10)',
@@ -506,7 +510,7 @@ const GrowthChart = memo(function GrowthChart({
]
}
return <Line data={data} options={CHART_OPTIONS} />
return <Line data={data} options={chartOptions} />
})
// ─── Main Component ───────────────────────────────────────────────────────────
@@ -698,135 +702,144 @@ export default function PlantSimulator() {
}
}, [running])
const t = useTranslations('plantSimulator')
const isFinished = plant.height >= MAX_HEIGHT
const statItems: { value: string | number; label: string; color: 'success' | 'warning' | 'error' | 'secondary' }[] = [
{ value: Math.round(plant.height), label: t('height'), color: 'success' },
{ value: plant.leaves.length, label: t('leaves'), color: 'warning' },
{ value: plant.branches.length, label: t('branches'), color: 'success' },
{ value: plant.fruits.length, label: t('fruits'), color: 'error' },
{ value: plant.yield.toFixed(1), label: t('yield'), color: 'warning' },
{ value: plant.yieldRate.toFixed(2), label: t('yieldRate'), color: 'secondary' }
]
return (
<div className='min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 p-6'>
<h1 className='text-3xl font-bold text-center mb-8 tracking-tight'>
🌱 شبیهساز رشد گیاه
</h1>
<div className='max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-6'>
<Box className='flex flex-col gap-6 is-full'>
<Typography variant='h4' className='text-center font-bold'>
🌱 {t('title')}
</Typography>
<Grid container spacing={6} className='max-w-6xl mx-auto'>
{/* ── Left: Plant visualization ── */}
<div className='lg:col-span-1 flex flex-col items-center gap-4'>
<div className='bg-slate-800 border border-slate-700 rounded-2xl p-6 w-full flex flex-col items-center shadow-xl'>
<Grid size={{ xs: 12, lg: 4 }} className='flex flex-col items-center gap-4'>
<Card className='is-full flex flex-col items-center p-6'>
<CardContent className='flex flex-col items-center gap-4 is-full p-0'>
<PlantSVG plant={plant} tick={plant.tick} running={running} />
{/* Stats */}
<div className='mt-4 grid grid-cols-3 gap-2 w-full text-sm'>
<div className='bg-green-900/40 border border-green-700/40 rounded-xl p-2.5 text-center'>
<div className='text-green-400 font-semibold text-base'>{Math.round(plant.height)}</div>
<div className='text-slate-400 text-[10px]'>ارتفاع</div>
</div>
<div className='bg-yellow-900/40 border border-yellow-700/40 rounded-xl p-2.5 text-center'>
<div className='text-yellow-400 font-semibold text-base'>{plant.leaves.length}</div>
<div className='text-slate-400 text-[10px]'>برگ</div>
</div>
<div className='bg-emerald-900/40 border border-emerald-700/40 rounded-xl p-2.5 text-center'>
<div className='text-emerald-400 font-semibold text-base'>{plant.branches.length}</div>
<div className='text-slate-400 text-[10px]'>شاخه</div>
</div>
<div className='bg-red-900/40 border border-red-700/40 rounded-xl p-2.5 text-center'>
<div className='text-red-400 font-semibold text-base'>{plant.fruits.length}</div>
<div className='text-slate-400 text-[10px]'>میوه</div>
</div>
<div className='bg-orange-900/40 border border-orange-700/40 rounded-xl p-2.5 text-center'>
<div className='text-orange-400 font-semibold text-base'>{plant.yield.toFixed(1)}</div>
<div className='text-slate-400 text-[10px]'>محصول (g)</div>
</div>
<div className='bg-violet-900/40 border border-violet-700/40 rounded-xl p-2.5 text-center'>
<div className='text-violet-400 font-semibold text-base'>{plant.yieldRate.toFixed(2)}</div>
<div className='text-slate-400 text-[10px]'>سرعت (g/s)</div>
</div>
</div>
<Grid container spacing={2} className='is-full'>
{statItems.map((item, idx) => (
<Grid key={idx} size={{ xs: 4 }}>
<Card variant='outlined' className='text-center p-2.5'>
<Typography variant='h6' color={item.color}>
{item.value}
</Typography>
<Typography variant='caption' color='text.secondary'>
{item.label}
</Typography>
</Card>
</Grid>
))}
</Grid>
{isFinished && (
<div className='mt-3 text-yellow-300 text-sm font-medium animate-pulse'>
🌼 گیاه به حداکثر رشد رسید!
</div>
<Typography variant='body2' color='warning.main' className='font-medium animate-pulse'>
🌼 {t('maxGrowthReached')}
</Typography>
)}
</div>
</CardContent>
</Card>
{/* Controls */}
<div className='bg-slate-800 border border-slate-700 rounded-2xl p-5 w-full shadow-xl space-y-4'>
<h2 className='font-semibold text-slate-300 text-base mb-1'>کنترلها</h2>
<Card className='is-full p-5'>
<CardContent className='space-y-4 p-0'>
<Typography variant='subtitle1' component='h2' className='font-semibold'>
{t('controls')}
</Typography>
{/* Start / Stop / Reset */}
<div className='flex gap-2'>
<button
<Box className='flex gap-2'>
<Button
variant='contained'
color={running ? 'error' : 'success'}
onClick={() => setRunning(r => !r)}
disabled={isFinished}
className={`flex-1 py-2 rounded-xl font-semibold transition-all text-sm
${running
? 'bg-red-600 hover:bg-red-500'
: 'bg-green-600 hover:bg-green-500'}
disabled:opacity-40 disabled:cursor-not-allowed`}
fullWidth
>
{running ? '⏸ توقف' : '▶ شروع'}
</button>
<button
{running ? `${t('stop')}` : `${t('start')}`}
</Button>
<Button
variant='outlined'
color='secondary'
onClick={() => { setRunning(false); reset() }}
className='px-4 py-2 rounded-xl bg-slate-600 hover:bg-slate-500 text-sm font-semibold transition-all'
>
ریست
</button>
</div>
{t('reset')}
</Button>
</Box>
{/* Speed slider */}
<div>
<label className='flex justify-between text-sm text-slate-400 mb-1'>
<span>سرعت رشد</span>
<span className='text-white font-medium'>{speed.toFixed(1)}×</span>
</label>
<Box>
<Typography variant='body2' color='text.secondary' className='flex justify-between mbe-1'>
<span>{t('growthSpeed')}</span>
<span className='font-medium'>{speed.toFixed(1)}×</span>
</Typography>
<input
type='range' min={0.5} max={5} step={0.5}
type='range'
min={0.5}
max={5}
step={0.5}
value={speed}
onChange={e => setSpeed(Number(e.target.value))}
className='w-full accent-green-500'
className='w-full'
/>
</div>
</Box>
{/* Light */}
<div>
<label className='flex justify-between text-sm text-slate-400 mb-1'>
<span> نور</span>
<span className='text-yellow-400 font-medium'>{env.light}%</span>
</label>
<Box>
<Typography variant='body2' color='text.secondary' className='flex justify-between mbe-1'>
<span> {t('light')}</span>
<span className='font-medium'>{env.light}%</span>
</Typography>
<input
type='range' min={0} max={100} step={5}
type='range'
min={0}
max={100}
step={5}
value={env.light}
onChange={e => setEnv(prev => ({ ...prev, light: Number(e.target.value) }))}
className='w-full accent-yellow-400'
className='w-full'
/>
</div>
</Box>
{/* Water */}
<div>
<label className='flex justify-between text-sm text-slate-400 mb-1'>
<span>💧 آب</span>
<span className='text-blue-400 font-medium'>{env.water}%</span>
</label>
<Box>
<Typography variant='body2' color='text.secondary' className='flex justify-between mbe-1'>
<span>💧 {t('water')}</span>
<span className='font-medium'>{env.water}%</span>
</Typography>
<input
type='range' min={0} max={100} step={5}
type='range'
min={0}
max={100}
step={5}
value={env.water}
onChange={e => setEnv(prev => ({ ...prev, water: Number(e.target.value) }))}
className='w-full accent-blue-400'
className='w-full'
/>
</div>
</Box>
{/* Effective rate indicator */}
<div className='bg-slate-700/50 rounded-xl p-3 text-xs text-slate-400'>
نرخ رشد مؤثر:{' '}
<span className='text-green-400 font-semibold'>
<Card variant='outlined' className='p-3'>
<Typography variant='caption' color='text.secondary'>
{t('effectiveRate')}{' '}
<Typography component='span' variant='caption' color='success.main' fontWeight='bold'>
{growthRate(env, speed).toFixed(2)}×
</span>
</div>
</div>
</div>
</Typography>
</Typography>
</Card>
</CardContent>
</Card>
</Grid>
{/* ── Right: Chart ── */}
<div className='lg:col-span-2 bg-slate-800 border border-slate-700 rounded-2xl p-6 shadow-xl'>
<Grid size={{ xs: 12, lg: 8 }}>
<Card className='p-6'>
<CardContent>
<GrowthChart
heightHistory={heightHistory}
leafHistory={leafHistory}
@@ -834,71 +847,87 @@ export default function PlantSimulator() {
yieldRateHistory={yieldRateHistory}
/>
{/* Info cards */}
<div className='mt-6 grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm'>
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
<div className='text-slate-400 mb-1'>پیشرفت رشد</div>
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
<div
className='bg-green-500 h-2 rounded-full transition-all'
style={{ width: `${(plant.height / MAX_HEIGHT) * 100}%` }}
<Grid container spacing={4} className='mt-6'>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<Card variant='outlined' className='p-4'>
<Typography variant='body2' color='text.secondary' className='mbe-1'>
{t('progressGrowth')}
</Typography>
<Box className='w-full rounded-full overflow-hidden bg-action-hover mbe-1' sx={{ height: 8 }}>
<Box
className='bg-success rounded-full h-full transition-all'
sx={{ width: `${(plant.height / MAX_HEIGHT) * 100}%` }}
/>
</div>
<div className='text-green-400 font-semibold'>
</Box>
<Typography variant='body2' color='success.main' fontWeight='bold'>
{Math.round((plant.height / MAX_HEIGHT) * 100)}%
</div>
</div>
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
<div className='text-slate-400 mb-1'>وضعیت نور</div>
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
<div
className='bg-yellow-400 h-2 rounded-full transition-all'
style={{ width: `${env.light}%` }}
</Typography>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<Card variant='outlined' className='p-4'>
<Typography variant='body2' color='text.secondary' className='mbe-1'>
{t('lightStatus')}
</Typography>
<Box className='w-full rounded-full overflow-hidden bg-action-hover mbe-1' sx={{ height: 8 }}>
<Box
className='bg-warning rounded-full h-full transition-all'
sx={{ width: `${env.light}%` }}
/>
</div>
<div className='text-yellow-400 font-semibold'>{env.light}%</div>
</div>
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
<div className='text-slate-400 mb-1'>وضعیت آب</div>
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
<div
className='bg-blue-400 h-2 rounded-full transition-all'
style={{ width: `${env.water}%` }}
</Box>
<Typography variant='body2' color='warning.main' fontWeight='bold'>
{env.light}%
</Typography>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<Card variant='outlined' className='p-4'>
<Typography variant='body2' color='text.secondary' className='mbe-1'>
{t('waterStatus')}
</Typography>
<Box className='w-full rounded-full overflow-hidden bg-action-hover mbe-1' sx={{ height: 8 }}>
<Box
className='bg-info rounded-full h-full transition-all'
sx={{ width: `${env.water}%` }}
/>
</div>
<div className='text-blue-400 font-semibold'>{env.water}%</div>
</div>
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
<div className='text-slate-400 mb-1'>محصول دهی</div>
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
<div
className='bg-orange-400 h-2 rounded-full transition-all'
style={{ width: `${(plant.yield / MAX_YIELD) * 100}%` }}
</Box>
<Typography variant='body2' color='info.main' fontWeight='bold'>
{env.water}%
</Typography>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<Card variant='outlined' className='p-4'>
<Typography variant='body2' color='text.secondary' className='mbe-1'>
{t('yieldStatus')}
</Typography>
<Box className='w-full rounded-full overflow-hidden bg-action-hover mbe-1' sx={{ height: 8 }}>
<Box
className='bg-warning rounded-full h-full transition-all'
sx={{ width: `${(plant.yield / MAX_YIELD) * 100}%` }}
/>
</div>
<div className='flex justify-between items-center'>
<span className='text-orange-400 font-semibold'>{plant.yield.toFixed(1)}g</span>
<span className='text-violet-400 text-xs'>{plant.yieldRate.toFixed(3)} g/s</span>
</div>
</div>
</div>
</Box>
<Box className='flex justify-between items-center'>
<Typography variant='body2' color='warning.main' fontWeight='bold'>
{plant.yield.toFixed(1)}g
</Typography>
<Typography variant='caption' color='secondary.main'>
{plant.yieldRate.toFixed(3)} g/s
</Typography>
</Box>
</Card>
</Grid>
</Grid>
{/* Description */}
<div className='mt-6 bg-slate-700/30 border border-slate-600/50 rounded-xl p-4 text-xs text-slate-400 leading-6'>
<p>
این شبیه‌ساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی
محاسبه می‌کند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی
در باد نمایش داده می‌شود. <strong className='text-slate-300'>محصول‌دهی (g)</strong> پس از ۲۰٪ رشد شروع شده
و با تعداد برگ، نور و آب شتاب می‌گیرد. <strong className='text-slate-300'>سرعت محصول (g/s)</strong> نشان‌دهنده
نرخ لحظه‌ای تولید است. نمودار تغییرات همه شاخص‌ها را در طول زمان ثبت می‌کند.
</p>
</div>
</div>
</div>
</div>
<Card variant='outlined' className='mt-6 p-4'>
<Typography variant='body2' color='text.secondary' className='leading-6'>
{t('description')}
</Typography>
</Card>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
)
}