Integrate next-intl for internationalization support across the application. Updated configuration to include next-intl plugin and modified components to utilize translation hooks for dynamic text rendering. Enhanced user experience by localizing navigation labels and form messages in various views, including login and dashboard components.

This commit is contained in:
2026-02-19 17:21:43 +03:30
parent 51175ffac2
commit 0844100613
34 changed files with 1372 additions and 248 deletions
+34 -32
View File
@@ -1,11 +1,12 @@
'use client'
// React Imports
import { useState } from 'react'
import { useMemo, useState } from 'react'
// Next Imports
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
// MUI Imports
import useMediaQuery from '@mui/material/useMediaQuery'
@@ -20,7 +21,6 @@ import { Controller, useForm } from 'react-hook-form'
import { valibotResolver } from '@hookform/resolvers/valibot'
import { object, minLength, maxLength, string, pipe, nonEmpty, custom } from 'valibot'
import type { SubmitHandler } from 'react-hook-form'
import type { InferInput } from 'valibot'
import classnames from 'classnames'
import { OTPInput } from 'input-otp'
@@ -76,22 +76,26 @@ type ErrorType = {
message: string
}
type PhoneFormData = InferInput<typeof phoneSchema>
const phoneSchema = object({
phone_number: pipe(
string(),
nonEmpty('Phone number is required'),
minLength(10, 'Phone number must be at least 10 digits'),
maxLength(15, 'Phone number must be at most 15 digits'),
custom(
(input) => /^[0-9]+$/.test(input),
'Phone number must contain only digits'
)
)
})
type PhoneFormData = {
phone_number: string
}
const Login = ({ mode }: { mode: SystemMode }) => {
const t = useTranslations('login')
const phoneSchema = useMemo(
() =>
object({
phone_number: pipe(
string(),
nonEmpty(String(t('validation.phoneRequired'))),
minLength(10, String(t('validation.phoneMinLength'))),
maxLength(15, String(t('validation.phoneMaxLength'))),
custom((input) => /^[0-9]+$/.test(String(input)), String(t('validation.phoneDigitsOnly')))
)
}),
[t]
)
// States
const [step, setStep] = useState<'phone' | 'otp'>('phone')
const [otp, setOtp] = useState('')
@@ -122,7 +126,7 @@ const Login = ({ mode }: { mode: SystemMode }) => {
getValues,
formState: { errors }
} = useForm<PhoneFormData>({
resolver: valibotResolver(phoneSchema),
resolver: valibotResolver(phoneSchema) as any,
defaultValues: {
phone_number: ''
}
@@ -145,7 +149,7 @@ const Login = ({ mode }: { mode: SystemMode }) => {
setTempToken(token)
setStep('otp')
} catch (error: any) {
setErrorState({ message: error.message || 'Failed to send OTP' })
setErrorState({ message: error.message || t('errors.sendOtpFailed') })
} finally {
setIsLoading(false)
}
@@ -153,7 +157,7 @@ const Login = ({ mode }: { mode: SystemMode }) => {
const onOtpSubmit = async () => {
if (otp.length !== 6) {
setErrorState({ message: 'Please enter the complete 6-digit OTP code' })
setErrorState({ message: t('errors.incompleteOtp') })
return
}
@@ -168,7 +172,7 @@ const Login = ({ mode }: { mode: SystemMode }) => {
const redirectURL = searchParams.get('redirectTo') ?? '/'
router.replace(redirectURL)
} catch (error: any) {
setErrorState({ message: error.message || 'OTP verification failed' })
setErrorState({ message: error.message || t('errors.otpVerificationFailed') })
} finally {
setIsLoading(false)
}
@@ -199,11 +203,9 @@ const Login = ({ mode }: { mode: SystemMode }) => {
</div>
<div className='flex flex-col gap-6 is-full sm:is-auto md:is-full sm:max-is-[400px] md:max-is-[unset] mbs-8 sm:mbs-11 md:mbs-0'>
<div className='flex flex-col gap-1'>
<Typography variant='h4'>{`Welcome to ${themeConfig.templateName}! 👋🏻`}</Typography>
<Typography variant='h4'>{t('welcome', { templateName: themeConfig.templateName })}</Typography>
<Typography>
{step === 'phone'
? 'Please enter your phone number to receive OTP'
: 'Please enter the OTP code sent to your phone'}
{step === 'phone' ? t('phoneStep') : t('otpStep')}
</Typography>
</div>
@@ -231,8 +233,8 @@ const Login = ({ mode }: { mode: SystemMode }) => {
autoFocus
fullWidth
type='tel'
label='Phone Number'
placeholder='Enter your phone number'
label={t('phoneNumber')}
placeholder={t('placeholderPhone')}
onChange={e => {
field.onChange(e.target.value.replace(/\D/g, '')) // Only allow digits
errorState !== null && setErrorState(null)
@@ -245,12 +247,12 @@ const Login = ({ mode }: { mode: SystemMode }) => {
)}
/>
<Button fullWidth variant='contained' type='submit' disabled={isLoading}>
{isLoading ? <CircularProgress size={24} /> : 'Send OTP'}
{isLoading ? <CircularProgress size={24} /> : t('sendOtp')}
</Button>
<div className='flex justify-center items-center flex-wrap gap-2'>
<Typography>New on our platform?</Typography>
<Typography>{t('newUser')}</Typography>
<Typography component={Link} href='/register' color='primary.main'>
Create an account
{t('createAccount')}
</Typography>
</div>
</form>
@@ -258,7 +260,7 @@ const Login = ({ mode }: { mode: SystemMode }) => {
<div className='flex flex-col gap-6'>
<div className='flex flex-col gap-4'>
<Typography variant='body2' color='text.secondary'>
OTP sent to {getValues('phone_number')}
{t('otpSent', { phone: getValues('phone_number') })}
</Typography>
<OtpContainer>
<OTPInput
@@ -289,10 +291,10 @@ const Login = ({ mode }: { mode: SystemMode }) => {
</OtpContainer>
</div>
<Button fullWidth variant='contained' onClick={onOtpSubmit} disabled={isLoading || otp.length !== 6}>
{isLoading ? <CircularProgress size={24} /> : 'Verify OTP'}
{isLoading ? <CircularProgress size={24} /> : t('verifyOtp')}
</Button>
<Button fullWidth variant='text' onClick={handleBackToPhone} disabled={isLoading}>
Back to Phone Number
{t('backToPhone')}
</Button>
</div>
)}
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
@@ -23,13 +26,14 @@ interface AnomalyDetectionCardProps {
}
const AnomalyDetectionCard = ({ data }: AnomalyDetectionCardProps) => {
const t = useTranslations('farmDashboard')
const anomalies = (data?.anomalies as AnomalyItem[] | undefined) ?? []
return (
<Card>
<CardHeader
avatar={<i className='tabler-alert-triangle text-xl' />}
title='Anomaly Detection'
title={t('cards.anomalyDetectionCard')}
subheader='Out of range values'
action={<OptionMenu options={['View All', 'Configure', 'Export']} />}
sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }}
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -35,6 +36,7 @@ interface EconomicOverviewProps {
}
const EconomicOverview = ({ data }: EconomicOverviewProps) => {
const t = useTranslations('farmDashboard')
const economicData = (data?.economicData as EconomicItem[] | undefined) ?? []
const chartSeries = (data?.chartSeries as Array<{ name: string; data: number[] }>) ?? []
const chartCategories = (data?.chartCategories as string[]) ?? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
@@ -74,7 +76,7 @@ const EconomicOverview = ({ data }: EconomicOverviewProps) => {
return (
<Card>
<CardHeader
title='Economic Overview'
title={t('cards.economicOverview')}
subheader='Costs & ROI'
action={<OptionMenu options={['Export PDF', 'Export Excel', 'Details']} />}
/>
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
@@ -39,13 +42,14 @@ interface FarmAlertsTimelineProps {
}
const FarmAlertsTimeline = ({ data }: FarmAlertsTimelineProps) => {
const t = useTranslations('farmDashboard')
const alerts = (data?.alerts as AlertItem[] | undefined) ?? []
return (
<Card>
<CardHeader
avatar={<i className='tabler-bell-ring text-xl' />}
title='AI Alerts'
title={t('cards.farmAlertsTimeline')}
titleTypographyProps={{ variant: 'h5' }}
subheader='Explainable recommendations'
action={<OptionMenu options={['View All', 'Configure', 'Export']} />}
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -36,6 +37,7 @@ interface FarmAlertsTrackerProps {
}
const FarmAlertsTracker = ({ data }: FarmAlertsTrackerProps) => {
const t = useTranslations('farmDashboard')
const alertStats = (data?.alertStats as AlertStatType[] | undefined) ?? []
const totalAlerts = (data?.totalAlerts as number | undefined) ?? 0
const radialBarValue = (data?.radialBarValue as number | undefined) ?? 30
@@ -94,7 +96,7 @@ const FarmAlertsTracker = ({ data }: FarmAlertsTrackerProps) => {
return (
<Card>
<CardHeader
title='Active Alerts'
title={t('cards.farmAlertsTracker')}
subheader='Requires Attention'
action={<OptionMenu options={['View All', 'Dismiss', 'Settings']} />}
/>
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// MUI Imports
import Drawer from '@mui/material/Drawer'
import Typography from '@mui/material/Typography'
@@ -33,6 +36,7 @@ type FarmDashboardSettingsDrawerProps = {
}
const FarmDashboardSettingsDrawer = (props: FarmDashboardSettingsDrawerProps) => {
const t = useTranslations('farmDashboard')
const { open, onClose, disabledCardIds, onToggleCard, cardLabels, rowLabels, rowCards } = props
const disabledSet = new Set(disabledCardIds)
@@ -55,9 +59,9 @@ const FarmDashboardSettingsDrawer = (props: FarmDashboardSettingsDrawerProps) =>
>
<Box className='flex flex-col is-full' sx={{ height: '100%' }}>
<Box className='p-6'>
<Typography variant='h5'>Dashboard Settings</Typography>
<Typography variant='h5'>{t('settings.title')}</Typography>
<Typography variant='body2' color='text.secondary' sx={{ mt: 0.5 }}>
Toggle cards to show or hide on the dashboard
{t('settings.toggleCards')}
</Typography>
</Box>
<Divider />
@@ -2,6 +2,7 @@
// React Imports
import { useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
// MUI Imports
import IconButton from '@mui/material/IconButton'
@@ -43,6 +44,7 @@ type FarmDashboardSettingsDropdownProps = {
}
const FarmDashboardSettingsDropdown = (props: FarmDashboardSettingsDropdownProps) => {
const t = useTranslations('farmDashboard')
const { disabledCardIds, onToggleCard, enableDragReorder, onToggleDragReorder, cardLabels, rowLabels, rowCards, saving } = props
const { settings } = useSettings()
const [open, setOpen] = useState(false)
@@ -59,7 +61,7 @@ const FarmDashboardSettingsDropdown = (props: FarmDashboardSettingsDropdownProps
<IconButton
ref={anchorRef}
onClick={handleToggle}
aria-label='Dashboard settings'
aria-label={t('settings.ariaLabel')}
className='text-textPrimary'
>
<i className='tabler-settings' />
@@ -82,9 +84,9 @@ const FarmDashboardSettingsDropdown = (props: FarmDashboardSettingsDropdownProps
<ClickAwayListener onClickAway={handleClose}>
<Box className='flex flex-col max-bs-[70vh]'>
<Box className='p-4'>
<Typography variant='h6'>Dashboard Settings</Typography>
<Typography variant='h6'>{t('settings.title')}</Typography>
<Typography variant='body2' color='text.secondary' sx={{ mt: 0.5 }}>
Toggle cards to show or hide
{t('settings.toggleCards')}
</Typography>
</Box>
<Box className='px-4 pb-2'>
@@ -97,7 +99,7 @@ const FarmDashboardSettingsDropdown = (props: FarmDashboardSettingsDropdownProps
/>
}
label={
<Typography variant='body2'>Enable drag & reorder rows</Typography>
<Typography variant='body2'>{t('settings.enableDragReorder')}</Typography>
}
sx={{ m: 0 }}
/>
@@ -2,7 +2,8 @@
// React Imports
import type { RefObject } from 'react'
import { useEffect, useState, useCallback, useContext } from 'react'
import { useEffect, useMemo, useState, useCallback, useContext } from 'react'
import { useTranslations } from 'next-intl'
// Context Imports
import NavbarSlotContext from '@/contexts/navbarSlotContext'
@@ -38,9 +39,7 @@ import EconomicOverview from '@views/dashboards/farm/EconomicOverview'
import {
ROW_IDS,
ROW_CARDS,
ROW_LABELS,
CARD_GRID_SIZE,
CARD_LABELS,
DEFAULT_FARM_DASHBOARD_CONFIG,
type RowId,
type CardId,
@@ -89,8 +88,55 @@ function mergeRowOrderAfterDrag(
}
const FarmDashboardWrapper = () => {
const t = useTranslations('farmDashboard')
const { setSlotContent } = useContext(NavbarSlotContext)
const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG)
const cardLabels = useMemo(
() =>
Object.fromEntries(
(
[
'farmOverviewKpis',
'farmWeatherCard',
'farmAlertsTracker',
'sensorValuesList',
'sensorRadarChart',
'sensorComparisonChart',
'anomalyDetectionCard',
'farmAlertsTimeline',
'waterNeedPrediction',
'harvestPredictionCard',
'yieldPredictionChart',
'soilMoistureHeatmap',
'ndviHealthCard',
'recommendationsList',
'economicOverview'
] as CardId[]
).map((id) => [id, t(`cards.${id}`)])
) as Record<CardId, string>,
[t]
)
const rowLabels = useMemo(
() =>
Object.fromEntries(
(
[
'overviewKpis',
'weatherAlerts',
'sensorMonitoring',
'sensorCharts',
'alertsWater',
'predictions',
'soilHeatmap',
'ndviRecommendations',
'economic'
] as RowId[]
).map((id) => [id, t(`rows.${id}`)])
) as Record<RowId, string>,
[t]
)
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
@@ -183,8 +229,8 @@ const FarmDashboardWrapper = () => {
onToggleCard={handleToggleCard}
enableDragReorder={config.enableDragReorder ?? true}
onToggleDragReorder={handleToggleDragReorder}
cardLabels={CARD_LABELS}
rowLabels={ROW_LABELS}
cardLabels={cardLabels}
rowLabels={rowLabels}
rowCards={ROW_CARDS}
saving={saving}
/>
@@ -231,7 +277,7 @@ const FarmDashboardWrapper = () => {
mt: 1,
'&:active': { cursor: 'grabbing' }
}}
aria-label={`Drag ${ROW_LABELS[rowId as RowId]}`}
aria-label={t('settings.dragRow', { row: rowLabels[rowId as RowId] })}
>
<i className='tabler-arrows-move text-textSecondary' />
</IconButton>
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -24,6 +25,7 @@ interface FarmWeatherCardProps {
}
const FarmWeatherCard = ({ data }: FarmWeatherCardProps) => {
const t = useTranslations('farmDashboard')
const temperature = data?.temperature ?? 24
const condition = (data?.condition as string) ?? ''
const humidity = data?.humidity ?? 45
@@ -86,7 +88,7 @@ const FarmWeatherCard = ({ data }: FarmWeatherCardProps) => {
return (
<Card className='pbe-6'>
<CardHeader
title='Weather Today'
title={t('cards.farmWeatherCard')}
subheader={condition ? `${condition}, ${temperature}${unit}` : `${temperature}${unit}`}
className='pbe-3'
action={<OptionMenu options={['Refresh', '7-day forecast', 'Details']} />}
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
@@ -16,6 +19,7 @@ interface HarvestPredictionCardProps {
}
const HarvestPredictionCard = ({ data }: HarvestPredictionCardProps) => {
const t = useTranslations('farmDashboard')
const harvestDate = (data?.dateFormatted as string) ?? ''
const daysUntil = (data?.daysUntil as number | undefined) ?? 0
const daysLeftFormatted = daysUntil > 0 ? `${daysUntil} days` : ''
@@ -29,7 +33,7 @@ const HarvestPredictionCard = ({ data }: HarvestPredictionCardProps) => {
<i className='tabler-calendar-event text-2xl' />
</CustomAvatar>
}
title='Harvest Prediction'
title={t('cards.harvestPredictionCard')}
subheader='AI Estimated Date'
action={<OptionMenu options={['Details', 'Adjust', 'Export']} />}
/>
+3 -1
View File
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -26,6 +27,7 @@ interface NDVIHealthCardProps {
}
const NDVIHealthCard = ({ data }: NDVIHealthCardProps) => {
const t = useTranslations('farmDashboard')
const ndviIndex = (data?.ndviIndex as number | undefined) ?? 0
const healthData =
(data?.healthData as Array<{ title: string; value: string; color: string; icon: string }>) ?? []
@@ -85,7 +87,7 @@ const NDVIHealthCard = ({ data }: NDVIHealthCardProps) => {
<Card>
<CardHeader
avatar={<i className='tabler-chart-radar text-xl' />}
title='NDVI Health'
title={t('cards.ndviHealthCard')}
subheader='Vegetation Index'
sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }}
/>
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
@@ -25,13 +28,14 @@ interface RecommendationsListProps {
}
const RecommendationsList = ({ data }: RecommendationsListProps) => {
const t = useTranslations('farmDashboard')
const recommendations = (data?.recommendations as RecommendationType[] | undefined) ?? []
if (recommendations.length === 0) return null
return (
<Card>
<CardHeader
title='AI Recommendations'
title={t('cards.recommendationsList')}
subheader='Action Items'
action={<OptionMenu options={['Export', 'Snooze', 'Mark Done']} />}
/>
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -21,6 +22,7 @@ interface SensorComparisonChartProps {
}
const SensorComparisonChart = ({ data }: SensorComparisonChartProps) => {
const t = useTranslations('farmDashboard')
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const categories = (data?.categories as string[]) ?? ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const currentValue = data?.currentValue ?? 48
@@ -75,7 +77,7 @@ const SensorComparisonChart = ({ data }: SensorComparisonChartProps) => {
return (
<Card>
<CardHeader
title='Soil Moisture Trend'
title={t('cards.sensorComparisonChart')}
subheader='Today vs Last Week'
/>
<CardContent>
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -23,6 +24,7 @@ interface SensorRadarChartProps {
}
const SensorRadarChart = ({ data }: SensorRadarChartProps) => {
const t = useTranslations('farmDashboard')
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const labels = (data?.labels as string[]) ?? []
const theme = useTheme()
@@ -70,7 +72,7 @@ const SensorRadarChart = ({ data }: SensorRadarChartProps) => {
return (
<Card>
<CardHeader
title='Sensor Comparison'
title={t('cards.sensorRadarChart')}
subheader='Today vs Ideal Ranges'
action={<OptionMenu options={['Today', 'This Week', 'This Month']} />}
/>
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
@@ -25,13 +28,14 @@ interface SensorValuesListProps {
}
const SensorValuesList = ({ data }: SensorValuesListProps) => {
const t = useTranslations('farmDashboard')
const sensors = (data?.sensors as SensorDataType[] | undefined) ?? []
if (sensors.length === 0) return null
return (
<Card>
<CardHeader
title='Environmental Sensors'
title={t('cards.sensorValuesList')}
subheader='Real-time Data'
action={<OptionMenu options={['Last Hour', 'Last 24h', 'Last 7 Days']} />}
/>
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -30,6 +31,7 @@ interface SoilMoistureHeatmapProps {
}
const SoilMoistureHeatmap = ({ data }: SoilMoistureHeatmapProps) => {
const t = useTranslations('farmDashboard')
const series = (data?.series as HeatmapSeries[]) ?? []
const theme = useTheme()
if (series.length === 0) return null
@@ -72,7 +74,7 @@ const SoilMoistureHeatmap = ({ data }: SoilMoistureHeatmapProps) => {
return (
<Card>
<CardHeader
title='Soil Moisture Heatmap'
title={t('cards.soilMoistureHeatmap')}
subheader='Field Zones by Time'
/>
<CardContent>
@@ -1,5 +1,8 @@
'use client'
// React Imports
import { useTranslations } from 'next-intl'
// Next Imports
import dynamic from 'next/dynamic'
@@ -24,6 +27,7 @@ interface WaterNeedPredictionProps {
}
const WaterNeedPrediction = ({ data }: WaterNeedPredictionProps) => {
const t = useTranslations('farmDashboard')
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const categories = (data?.categories as string[]) ?? ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7']
const totalNext7Days = data?.totalNext7Days ?? 0
@@ -74,7 +78,7 @@ const WaterNeedPrediction = ({ data }: WaterNeedPredictionProps) => {
return (
<Card>
<CardHeader
title='7-Day Water Need Prediction'
title={t('cards.waterNeedPrediction')}
subheader='AI Forecast'
action={<OptionMenu options={['Export', 'Adjust', 'Details']} />}
/>
@@ -2,6 +2,7 @@
// Next Imports
import dynamic from 'next/dynamic'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -34,6 +35,7 @@ interface YieldPredictionChartProps {
}
const YieldPredictionChart = ({ data }: YieldPredictionChartProps) => {
const t = useTranslations('farmDashboard')
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const categories =
(data?.categories as string[]) ??
@@ -77,7 +79,7 @@ const YieldPredictionChart = ({ data }: YieldPredictionChartProps) => {
return (
<Card>
<CardHeader
title='Yield Prediction'
title={t('cards.yieldPredictionChart')}
subheader='This Year vs Last Year'
action={<OptionMenu options={['Export', 'Compare', 'Details']} />}
/>
@@ -2,6 +2,7 @@
// React Imports
import { useState } from 'react'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -20,7 +21,7 @@ import { useForm, Controller } from 'react-hook-form'
import ConfirmationDialog from '@components/dialogs/confirmation-dialog'
const AccountDelete = () => {
// States
const t = useTranslations('accountSettings')
const [open, setOpen] = useState(false)
// Hooks
@@ -40,7 +41,7 @@ const AccountDelete = () => {
return (
<Card>
<CardHeader title='Delete Account' />
<CardHeader title={t('deleteAccount')} />
<CardContent>
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl error={Boolean(errors.checkbox)} className='is-full mbe-6'>
@@ -49,13 +50,13 @@ const AccountDelete = () => {
control={control}
rules={{ required: true }}
render={({ field }) => (
<FormControlLabel control={<Checkbox {...field} />} label='I confirm my account deactivation' />
<FormControlLabel control={<Checkbox {...field} />} label={t('confirmDeactivation')} />
)}
/>
{errors.checkbox && <FormHelperText error>Please confirm you want to delete account</FormHelperText>}
{errors.checkbox && <FormHelperText error>{t('confirmDelete')}</FormHelperText>}
</FormControl>
<Button variant='contained' color='error' type='submit' disabled={!checkboxValue}>
Deactivate Account
{t('deactivateAccount')}
</Button>
<ConfirmationDialog open={open} setOpen={setOpen} type='delete-account' />
</form>
@@ -2,6 +2,7 @@
// React Imports
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import type { ChangeEvent } from 'react'
// MUI Imports
@@ -36,7 +37,7 @@ const initialData: Data = {
}
const AccountDetails = () => {
// Auth
const t = useTranslations('accountSettings')
const { user: authUser } = useAuth()
// States
@@ -98,7 +99,7 @@ const AccountDetails = () => {
})
// Refresh auth user data - caller would need to update context; for now form stays as-is
} catch (err: any) {
setError(err.message || 'خطا در ذخیره تغییرات')
setError(err.message || t('errorSave'))
} finally {
setSaving(false)
}
@@ -171,45 +172,45 @@ const AccountDetails = () => {
<Grid size={{ xs: 12, sm: 6 }}>
<CustomTextField
fullWidth
label='First Name'
label={t('firstName')}
value={formData.firstName}
placeholder='John'
placeholder={t('placeholderFirstName')}
onChange={e => handleFormChange('firstName', e.target.value)}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<CustomTextField
fullWidth
label='Last Name'
label={t('lastName')}
value={formData.lastName}
placeholder='Doe'
placeholder={t('placeholderLastName')}
onChange={e => handleFormChange('lastName', e.target.value)}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<CustomTextField
fullWidth
label='Email'
label={t('email')}
value={formData.email}
placeholder='john.doe@gmail.com'
placeholder={t('placeholderEmail')}
onChange={e => handleFormChange('email', e.target.value)}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<CustomTextField
fullWidth
label='Phone Number'
label={t('phoneNumber')}
value={formData.phoneNumber}
placeholder='+1 (234) 567-8901'
placeholder={t('placeholderPhone')}
onChange={e => handleFormChange('phoneNumber', e.target.value)}
/>
</Grid>
<Grid size={{ xs: 12 }} className='flex gap-4 flex-wrap'>
<Button variant='contained' type='submit' disabled={saving}>
{saving ? 'در حال ذخیره...' : 'Save Changes'}
{saving ? t('saving') : t('saveChanges')}
</Button>
<Button variant='tonal' type='button' color='secondary' onClick={handleReset}>
Reset
{t('reset')}
</Button>
</Grid>
</Grid>
+4 -3
View File
@@ -2,6 +2,7 @@
// React Imports
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import type { SyntheticEvent, ReactElement } from 'react'
// MUI Imports
@@ -14,7 +15,7 @@ import TabPanel from '@mui/lab/TabPanel'
import CustomTabList from '@core/components/mui/TabList'
const AccountSettings = ({ tabContentList }: { tabContentList: { [key: string]: ReactElement } }) => {
// States
const t = useTranslations('accountSettings')
const [activeTab, setActiveTab] = useState('account')
const handleChange = (event: SyntheticEvent, value: string) => {
@@ -26,8 +27,8 @@ const AccountSettings = ({ tabContentList }: { tabContentList: { [key: string]:
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<CustomTabList onChange={handleChange} variant='scrollable' pill='true'>
<Tab label='Account' icon={<i className='tabler-users' />} iconPosition='start' value='account' />
<Tab label='SensorHub' icon={<i className='tabler-device-watch' />} iconPosition='start' value='sensor-hub' />
<Tab label={t('account')} icon={<i className='tabler-users' />} iconPosition='start' value='account' />
<Tab label={t('sensorHub')} icon={<i className='tabler-device-watch' />} iconPosition='start' value='sensor-hub' />
{/* <Tab label='Security' icon={<i className='tabler-lock' />} iconPosition='start' value='security' />
<Tab
label='Billing & Plans'
@@ -2,6 +2,7 @@
// React Imports
import { useState } from 'react'
import { useTranslations } from 'next-intl'
// MUI Imports
import Grid from '@mui/material/Grid2'
@@ -25,6 +26,7 @@ import FormSensorHub from '@views/sensorHub/FormSensorHub'
const transitionTimeout = { enter: 300, exit: 200 }
const SensorHubTabContent = () => {
const t = useTranslations('sensorHub')
const [showAddForm, setShowAddForm] = useState(false)
const { setSensorHub } = useSensorHub()
@@ -49,10 +51,10 @@ const SensorHubTabContent = () => {
<div className='grid grid-cols-1 sm:grid-cols-[1fr_auto] items-center gap-4'>
<div className='flex flex-col gap-0.5'>
<Typography variant='h6' fontWeight={600}>
انتخاب سنسور
{t('selectSensor')}
</Typography>
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید
{t('selectSensorDescription')}
</Typography>
</div>
<Button
@@ -61,7 +63,7 @@ const SensorHubTabContent = () => {
startIcon={<i className='tabler-plus text-xl' />}
onClick={() => setShowAddForm(true)}
>
اضافه کردن سنسور
{t('addSensor')}
</Button>
</div>
<OptionSensorHub onConfirm={handleConfirm} />
+10 -8
View File
@@ -2,6 +2,7 @@
// React Imports
import { useState } from 'react'
import { useTranslations } from 'next-intl'
// MUI Imports
import Grid from '@mui/material/Grid2'
@@ -20,6 +21,7 @@ type FormSensorHubProps = {
}
const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
const t = useTranslations('sensorHub')
const [name, setName] = useState('')
const [uuidSensor, setUuidSensor] = useState('')
const [loading, setLoading] = useState(false)
@@ -33,7 +35,7 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
await sensorHubService.addSensor({ name, uuid_sensor: uuidSensor })
onBack()
} catch (err: unknown) {
const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: string }).message) : 'خطا در ذخیره سنسور'
const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: string }).message) : t('errorSave')
setError(message)
} finally {
setLoading(false)
@@ -50,7 +52,7 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
startIcon={<i className='tabler-arrow-right text-xl' />}
onClick={onBack}
>
بازگشت
{t('back')}
</Button>
{/* <Typography variant='h6'>افزودن سنسور جدید</Typography> */}
</div>
@@ -66,8 +68,8 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
<Grid size={{ xs: 12 }}>
<CustomTextField
fullWidth
label='نام سنسور'
placeholder='نام سنسور را وارد کنید'
label={t('sensorName')}
placeholder={t('placeholderName')}
value={name}
onChange={e => setName(e.target.value)}
/>
@@ -75,15 +77,15 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
<Grid size={{ xs: 12 }}>
<CustomTextField
fullWidth
label='شناسه سنسور (UUID)'
placeholder='شناسه سنسور را وارد کنید'
label={t('sensorUuid')}
placeholder={t('placeholderUuid')}
value={uuidSensor}
onChange={e => setUuidSensor(e.target.value)}
/>
</Grid>
<Grid size={{ xs: 12 }} className='flex gap-2'>
<Button variant='tonal' color='secondary' onClick={onBack} disabled={loading}>
انصراف
{t('cancel')}
</Button>
<Button
variant='contained'
@@ -91,7 +93,7 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
disabled={loading}
startIcon={loading ? <CircularProgress size={16} color='inherit' /> : <i className='tabler-plus' />}
>
{loading ? 'در حال ذخیره...' : 'ذخیره سنسور'}
{loading ? t('saving') : t('saveSensor')}
</Button>
</Grid>
</Grid>
+25 -20
View File
@@ -1,7 +1,8 @@
'use client'
// React Imports
import { useState, useEffect } from 'react'
import { useMemo, useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
// MUI Imports
import Card from '@mui/material/Card'
@@ -33,22 +34,26 @@ const formatToShamsi = (dateStr: string | null | undefined): string => {
// Column Definitions
const columnHelper = createColumnHelper<Sensor>()
const columns = [
columnHelper.accessor('name', {
cell: info => info.getValue(),
header: 'Name'
}),
columnHelper.accessor('last_updated', {
cell: info => formatToShamsi(info.getValue()),
header: 'Last Update'
}),
columnHelper.accessor('uuid_sensor', {
cell: info => info.getValue(),
header: 'UUID'
})
]
const SensorHubTable = () => {
const t = useTranslations('sensorHub')
const columns = useMemo(
() => [
columnHelper.accessor('name', {
cell: info => info.getValue(),
header: t('columns.name')
}),
columnHelper.accessor('last_updated', {
cell: info => formatToShamsi(info.getValue()),
header: t('columns.lastUpdate')
}),
columnHelper.accessor('uuid_sensor', {
cell: info => info.getValue(),
header: t('columns.uuid')
})
],
[t]
)
const [data, setData] = useState<Sensor[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -61,7 +66,7 @@ const SensorHubTable = () => {
const sensors = await sensorHubService.listSensors()
setData(sensors)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load sensors')
setError(err instanceof Error ? err.message : t('errorLoad'))
setData([])
} finally {
setLoading(false)
@@ -83,7 +88,7 @@ const SensorHubTable = () => {
if (loading) {
return (
<Card>
<CardHeader title='Sensor Hub' />
<CardHeader title={t('title')} />
<div className='flex items-center justify-center p-12'>
<CircularProgress />
</div>
@@ -94,14 +99,14 @@ const SensorHubTable = () => {
if (error) {
return (
<Card>
<CardHeader title='Sensor Hub' subheader={error} />
<CardHeader title={t('title')} subheader={error} />
</Card>
)
}
return (
<Card>
<CardHeader title='Sensor Hub' />
<CardHeader title={t('title')} />
<div className='overflow-x-auto'>
<table className={styles.table}>
<thead>
+32 -10
View File
@@ -3,6 +3,7 @@
// React Imports
import { useState } from 'react'
import type { Theme } from '@mui/material/styles'
import { useTranslations } from 'next-intl'
// Hook Imports
import { useSensorHub } from '@/hooks/useSensorHub'
@@ -35,12 +36,18 @@ const DialogContentWithTransition = ({
showAddForm,
onShowAddForm,
onBack,
onConfirm
onConfirm,
selectSensor,
selectSensorDescription,
addSensor
}: {
showAddForm: boolean
onShowAddForm: () => void
onBack: () => void
onConfirm: (sensor: Sensor) => void
selectSensor: string
selectSensorDescription: string
addSensor: string
}) => (
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
<div>
@@ -51,10 +58,10 @@ const DialogContentWithTransition = ({
<div className='grid grid-cols-[1fr_auto] items-center gap-4'>
<div className='flex flex-col gap-0.5'>
<Typography variant='h6' fontWeight={600}>
انتخاب سنسور
{selectSensor}
</Typography>
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید
{selectSensorDescription}
</Typography>
</div>
<Button
@@ -63,7 +70,7 @@ const DialogContentWithTransition = ({
startIcon={<i className='tabler-plus text-xl' />}
onClick={onShowAddForm}
>
اضافه کردن سنسور
{addSensor}
</Button>
</div>
<OptionSensorHub onConfirm={onConfirm} />
@@ -77,12 +84,18 @@ const DrawerContentWithTransition = ({
showAddForm,
onShowAddForm,
onBack,
onConfirm
onConfirm,
selectSensor,
selectSensorDescription,
addSensor
}: {
showAddForm: boolean
onShowAddForm: () => void
onBack: () => void
onConfirm: (sensor: Sensor) => void
selectSensor: string
selectSensorDescription: string
addSensor: string
}) => (
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
<div>
@@ -93,10 +106,10 @@ const DrawerContentWithTransition = ({
<div className='grid grid-cols-[1fr_auto] items-center gap-4'>
<div className='flex flex-col gap-0.5'>
<Typography variant='h6' fontWeight={600}>
انتخاب سنسور
{selectSensor}
</Typography>
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید
{selectSensorDescription}
</Typography>
</div>
<Button
@@ -105,7 +118,7 @@ const DrawerContentWithTransition = ({
startIcon={<i className='tabler-plus text-xl' />}
onClick={onShowAddForm}
>
اضافه کردن سنسور
{addSensor}
</Button>
</div>
<OptionSensorHub onConfirm={onConfirm} />
@@ -116,10 +129,17 @@ const DrawerContentWithTransition = ({
)
const TableModalSheet = ({ open, onClose }: TableModalSheetProps) => {
const t = useTranslations('sensorHub')
const [showAddForm, setShowAddForm] = useState(false)
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
const { setSensorHub } = useSensorHub()
const contentProps = {
selectSensor: t('selectSensor'),
selectSensorDescription: t('selectSensorDescription'),
addSensor: t('addSensor')
}
const handleBack = () => setShowAddForm(false)
const handleConfirm = (sensor: Sensor) => {
@@ -149,8 +169,8 @@ const TableModalSheet = ({ open, onClose }: TableModalSheetProps) => {
}}
>
<div className='flex items-center justify-between plb-4 pli-6 border-bs'>
<Typography variant='h5'>Sensor Data</Typography>
<IconButton size='small' onClick={onClose} aria-label='close'>
<Typography variant='h5'>{t('sensorData')}</Typography>
<IconButton size='small' onClick={onClose} aria-label={t('ariaClose')}>
<i className='tabler-x text-2xl' />
</IconButton>
</div>
@@ -160,6 +180,7 @@ const TableModalSheet = ({ open, onClose }: TableModalSheetProps) => {
onShowAddForm={() => setShowAddForm(true)}
onBack={handleBack}
onConfirm={handleConfirm}
{...contentProps}
/>
</div>
</Drawer>
@@ -185,6 +206,7 @@ const TableModalSheet = ({ open, onClose }: TableModalSheetProps) => {
onShowAddForm={() => setShowAddForm(true)}
onBack={handleBack}
onConfirm={handleConfirm}
{...contentProps}
/>
</DialogContent>
</Dialog>