i18n: extract things, history, projects, labels, filters, timer and points
Part of #145. Fourteen files across seven feature areas that had no namespace yet: the things create/edit modals and their history, the chore history detail and edit modals, the activity feed, the points view and its redemption modal, the project view with its selector and icon picker, the label view, the advanced filter builder and the timer edit modal. Seven new namespaces registered in `src/i18n/config.js` in one change rather than one per PR, so the `ns:` array is touched once and my other extraction PRs cannot conflict with this one. Namespaces stay feature-scoped as described in #145; if you'd rather fold any of these into `common` or `chores`, say which and I'll rework it. Dictionaries: `history` 57 keys, `points` 50, `timer` 25, `projects` 16, `things` 14, `filters` 13, `labels` 5. English only — no translations, no behaviour change. Every t() value is checked against this branch's base: the string must appear character-for-character in the code it replaces (226 call sites). Three values in `UserPoints` are matched loosely and worth naming. The base builds the leaderboard heading and subtitle around a ternary — `{mode === 'points' ? 'Points' : 'Tasks'} Leaderboard` and `Rankings based on {…} during the selected time period` — so neither full sentence exists contiguously in the source. Each key holds exactly what one branch renders. The sentences are kept whole rather than split around the ternary, since a sentence assembled from fragments cannot be reordered by a translator.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { FormLabel, Input } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import ModalActions from '../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
import ConfirmationModal from './Inputs/ConfirmationModal'
|
||||
|
||||
function EditHistoryModal({ config, historyRecord }) {
|
||||
const { t } = useTranslation('history')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [completedDate, setCompletedDate] = useState('')
|
||||
@@ -40,12 +42,12 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
onClose={config?.onClose}
|
||||
size='lg'
|
||||
// fullWidth={true}
|
||||
title='Edit History'
|
||||
title={t('edit.title')}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: config.onClose }}
|
||||
secondary={{ label: t('common:cancel'), onClick: config.onClose }}
|
||||
primary={{
|
||||
label: 'Save',
|
||||
label: t('common:save'),
|
||||
onClick: () =>
|
||||
config.onSave({
|
||||
id: historyRecord.id,
|
||||
@@ -57,7 +59,7 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormLabel>Due Date</FormLabel>
|
||||
<FormLabel>{t('chores:dueDate')}</FormLabel>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
value={dueDate}
|
||||
@@ -66,7 +68,7 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
}}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<FormLabel>Completed Date</FormLabel>
|
||||
<FormLabel>{t('edit.completedDate')}</FormLabel>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
value={completedDate}
|
||||
@@ -75,12 +77,12 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
}}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<FormLabel>Note</FormLabel>
|
||||
<FormLabel>{t('edit.note')}</FormLabel>
|
||||
<Input
|
||||
fullWidth
|
||||
multiline
|
||||
label='Additional Notes'
|
||||
placeholder='Additional Notes'
|
||||
label={t('edit.additionalNotes')}
|
||||
placeholder={t('edit.additionalNotes')}
|
||||
value={notes}
|
||||
onChange={e => {
|
||||
if (e.target.value.trim() === '') {
|
||||
@@ -104,10 +106,10 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
}
|
||||
setIsDeleteModalOpen(false)
|
||||
},
|
||||
title: 'Delete History',
|
||||
message: 'Are you sure you want to delete this history?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
title: t('edit.deleteTitle'),
|
||||
message: t('edit.deleteMessage'),
|
||||
confirmText: t('common:delete'),
|
||||
cancelText: t('common:cancel'),
|
||||
color: 'danger',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@mui/icons-material'
|
||||
import { Avatar, Box, Button, Chip, Divider, Stack, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import ModalActions from '../../components/common/ModalActions'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
@@ -22,13 +23,13 @@ import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||
import RichTextEditor from '../components/RichTextEditor.jsx'
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
0: { label: 'In Progress', color: 'primary', icon: <AccessTime /> },
|
||||
1: { label: 'Completed', color: 'success', icon: <Check /> },
|
||||
2: { label: 'Skipped', color: 'warning', icon: <Redo /> },
|
||||
3: { label: 'Pending Approval', color: 'neutral', icon: <HourglassEmpty /> },
|
||||
4: { label: 'Rejected', color: 'danger', icon: <ThumbDown /> },
|
||||
5: { label: 'Missed', color: 'danger', icon: <RunningWithErrors /> },
|
||||
6: { label: 'Rescheduled', color: 'warning', icon: <Schedule /> },
|
||||
0: { label: t('status.inProgress'), color: 'primary', icon: <AccessTime /> },
|
||||
1: { label: t('status.completed'), color: 'success', icon: <Check /> },
|
||||
2: { label: t('status.skipped'), color: 'warning', icon: <Redo /> },
|
||||
3: { label: t('status.pendingApproval'), color: 'neutral', icon: <HourglassEmpty /> },
|
||||
4: { label: t('status.rejected'), color: 'danger', icon: <ThumbDown /> },
|
||||
5: { label: t('status.missed'), color: 'danger', icon: <RunningWithErrors /> },
|
||||
6: { label: t('status.rescheduled'), color: 'warning', icon: <Schedule /> },
|
||||
}
|
||||
|
||||
const DetailRow = ({ icon, label, value, children }) => (
|
||||
@@ -55,6 +56,8 @@ const DetailRow = ({ icon, label, value, children }) => (
|
||||
)
|
||||
|
||||
const TimingBadge = ({ historyEntry }) => {
|
||||
const { t } = useTranslation('history')
|
||||
|
||||
if (!historyEntry.dueDate || !historyEntry.performedAt) return null
|
||||
if ([0, 5, 6].includes(historyEntry.status)) return null
|
||||
|
||||
@@ -71,7 +74,7 @@ const TimingBadge = ({ historyEntry }) => {
|
||||
sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }}
|
||||
startDecorator={<Check />}
|
||||
>
|
||||
On Time
|
||||
{t('badge.onTime')}
|
||||
</Chip>
|
||||
)
|
||||
} else if (performedAt.isBefore(dueDate)) {
|
||||
@@ -103,6 +106,7 @@ const TimingBadge = ({ historyEntry }) => {
|
||||
}
|
||||
|
||||
function HistoryDetailModal({ config }) {
|
||||
const { t } = useTranslation('history')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const { fmt } = useLocalization()
|
||||
const navigate = useNavigate()
|
||||
@@ -114,7 +118,9 @@ function HistoryDetailModal({ config }) {
|
||||
|
||||
const statusCfg = STATUS_CONFIG[entry.status] ?? STATUS_CONFIG[1]
|
||||
const isFirstSchedule = entry.status === 6 && !entry.dueDate
|
||||
const statusLabel = isFirstSchedule ? 'Scheduled' : statusCfg.label
|
||||
const statusLabel = isFirstSchedule
|
||||
? t('status.scheduled')
|
||||
: t(statusCfg.labelKey)
|
||||
const performer = performers.find(p => p.userId === entry.completedBy)
|
||||
const assignedTo = performers.find(p => p.userId === entry.assignedTo)
|
||||
const isDifferentAssignee =
|
||||
@@ -138,7 +144,7 @@ function HistoryDetailModal({ config }) {
|
||||
<ResponsiveModal
|
||||
open={config?.isOpen}
|
||||
onClose={config?.onClose}
|
||||
title='Activity Detail'
|
||||
title={t('detail.title')}
|
||||
footer={
|
||||
<ModalActions>
|
||||
{entry.choreId && (
|
||||
@@ -151,7 +157,7 @@ function HistoryDetailModal({ config }) {
|
||||
navigate(`/chores/${entry.choreId}`)
|
||||
}}
|
||||
>
|
||||
Open Task
|
||||
{t('detail.openTask')}
|
||||
</Button>
|
||||
)}
|
||||
{config?.onEdit && (
|
||||
@@ -159,7 +165,7 @@ function HistoryDetailModal({ config }) {
|
||||
startDecorator={<Edit sx={{ fontSize: 16 }} />}
|
||||
onClick={() => config.onEdit(entry)}
|
||||
>
|
||||
Edit Entry
|
||||
{t('detail.editEntry')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalActions>
|
||||
@@ -196,7 +202,7 @@ function HistoryDetailModal({ config }) {
|
||||
{performer && (
|
||||
<DetailRow
|
||||
icon={<Check sx={{ fontSize: 16 }} />}
|
||||
label='Performed by'
|
||||
label={t('detail.performedBy')}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Avatar
|
||||
@@ -216,7 +222,7 @@ function HistoryDetailModal({ config }) {
|
||||
{isDifferentAssignee && assignedTo && (
|
||||
<DetailRow
|
||||
icon={<Person sx={{ fontSize: 16 }} />}
|
||||
label='Assigned to'
|
||||
label={t('detail.assignedTo')}
|
||||
value={assignedTo.displayName}
|
||||
/>
|
||||
)}
|
||||
@@ -259,7 +265,7 @@ function HistoryDetailModal({ config }) {
|
||||
{showUpdatedAt && (
|
||||
<DetailRow
|
||||
icon={<Update sx={{ fontSize: 16 }} />}
|
||||
label='Last updated'
|
||||
label={t('detail.lastUpdated')}
|
||||
value={fmt.dateTime(entry.updatedAt)}
|
||||
/>
|
||||
)}
|
||||
@@ -268,7 +274,7 @@ function HistoryDetailModal({ config }) {
|
||||
{entry.duration > 0 && (
|
||||
<DetailRow
|
||||
icon={<Schedule sx={{ fontSize: 16 }} />}
|
||||
label='Duration'
|
||||
label={t('detail.duration')}
|
||||
value={formatDuration(entry.duration)}
|
||||
/>
|
||||
)}
|
||||
@@ -277,8 +283,8 @@ function HistoryDetailModal({ config }) {
|
||||
{entry.points > 0 && (
|
||||
<DetailRow
|
||||
icon={<Typography sx={{ fontSize: 14 }}>★</Typography>}
|
||||
label='Points earned'
|
||||
value={`${entry.points} pt${entry.points > 1 ? 's' : ''}`}
|
||||
label={t('detail.pointsEarned')}
|
||||
value={t('detail.points', { count: entry.points })}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import FilterBuilderContent, {
|
||||
import { FILTER_COLORS } from '../../../utils/Colors'
|
||||
import { applyFilter } from '../../../utils/FilterEngine'
|
||||
import { useFilters } from '../../Filters/FilterQueries'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const EMPTY_FILTERS = []
|
||||
|
||||
@@ -25,6 +26,7 @@ const AdvancedFilterBuilder = ({
|
||||
userProfile = null,
|
||||
editingFilter = null,
|
||||
}) => {
|
||||
const { t } = useTranslation('filters')
|
||||
const [filterName, setFilterName] = useState('')
|
||||
const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value)
|
||||
const [selections, setSelections] = useState(defaultSelections())
|
||||
@@ -76,15 +78,15 @@ const AdvancedFilterBuilder = ({
|
||||
|
||||
const handleSave = () => {
|
||||
if (!filterName.trim()) {
|
||||
setError('Please enter a filter name')
|
||||
setError(t('builder.errorName'))
|
||||
return
|
||||
}
|
||||
if (filterNameExists(filterName.trim(), editingFilter?.id)) {
|
||||
setError('A filter with this name already exists')
|
||||
setError(t('nameExists'))
|
||||
return
|
||||
}
|
||||
if (conditions.length === 0) {
|
||||
setError('Please configure at least one filter condition')
|
||||
setError(t('builder.errorConditions'))
|
||||
return
|
||||
}
|
||||
onSave({
|
||||
@@ -106,7 +108,7 @@ const AdvancedFilterBuilder = ({
|
||||
maxHeight='92vh'
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{editingFilter ? 'Edit Filter' : 'New Filter'}
|
||||
{editingFilter ? t('builder.editTitle') : t('builder.newTitle')}
|
||||
{activeConditionCount > 0 && (
|
||||
<Chip size='sm' variant='solid' color='primary'>
|
||||
{activeConditionCount} condition
|
||||
@@ -129,17 +131,17 @@ const AdvancedFilterBuilder = ({
|
||||
{conditions.length > 0 ? (
|
||||
<>
|
||||
<Chip size='sm' variant='soft' color='neutral'>
|
||||
{previewCount} task{previewCount !== 1 ? 's' : ''}
|
||||
{t('tasks', { count: previewCount })}
|
||||
</Chip>
|
||||
{previewOverdueCount > 0 && (
|
||||
<Chip size='sm' variant='solid' color='danger'>
|
||||
{previewOverdueCount} overdue
|
||||
{t('overdue', { count: previewOverdueCount })}
|
||||
</Chip>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
||||
Add conditions to preview
|
||||
{t('builder.addConditions')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -147,7 +149,7 @@ const AdvancedFilterBuilder = ({
|
||||
{/* Actions */}
|
||||
<ModalActions>
|
||||
<Button variant='outlined' color='neutral' onClick={onClose}>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
@@ -155,7 +157,7 @@ const AdvancedFilterBuilder = ({
|
||||
startDecorator={<Save sx={{ fontSize: 16 }} />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Filter
|
||||
{t('builder.save')}
|
||||
</Button>
|
||||
</ModalActions>
|
||||
</Box>
|
||||
@@ -168,10 +170,10 @@ const AdvancedFilterBuilder = ({
|
||||
level='body-xs'
|
||||
sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}
|
||||
>
|
||||
Filter Name
|
||||
{t('builder.name')}
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder='e.g. Overdue tasks for Alice'
|
||||
placeholder={t('builder.namePlaceholder')}
|
||||
value={filterName}
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value)
|
||||
@@ -193,13 +195,13 @@ const AdvancedFilterBuilder = ({
|
||||
level='body-xs'
|
||||
sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}
|
||||
>
|
||||
Color
|
||||
{t('builder.color')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{FILTER_COLORS.map(c => (
|
||||
<Box
|
||||
key={c.value}
|
||||
title={c.name}
|
||||
title={t(`common:colors.${c.key}`)}
|
||||
onClick={() => setFilterColor(c.value)}
|
||||
sx={{
|
||||
width: 26,
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const { t } = useTranslation('things')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [name, setName] = useState(currentThing?.name || '')
|
||||
@@ -33,17 +35,17 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const isValid = () => {
|
||||
const newErrors = {}
|
||||
if (!name || name.trim() === '') {
|
||||
newErrors.name = 'Name is required'
|
||||
newErrors.name = t('errName')
|
||||
}
|
||||
|
||||
if (type === 'number' && isNaN(state)) {
|
||||
newErrors.state = 'State must be a number'
|
||||
newErrors.state = t('errStateNumber')
|
||||
}
|
||||
if (type === 'boolean' && !['true', 'false'].includes(state)) {
|
||||
newErrors.state = 'State must be true or false'
|
||||
newErrors.state = t('errStateBool')
|
||||
}
|
||||
if ((type === 'text' && !state) || state.trim() === '') {
|
||||
newErrors.state = 'State is required'
|
||||
newErrors.state = t('errStateRequired')
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
@@ -67,7 +69,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
label: t('common:cancel'),
|
||||
onClick: onClose,
|
||||
}}
|
||||
primary={{
|
||||
@@ -78,9 +80,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography>Name</Typography>
|
||||
<Typography>{t('name')}</Typography>
|
||||
<Textarea
|
||||
placeholder='Thing name'
|
||||
placeholder={t('namePlaceholder')}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
@@ -101,9 +103,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
</FormControl>
|
||||
{type === 'text' && (
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
<Typography>{t('value')}</Typography>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
placeholder={t('valuePlaceholder')}
|
||||
value={state || ''}
|
||||
onChange={e => setState(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
@@ -113,9 +115,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
)}
|
||||
{type === 'number' && (
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
<Typography>{t('value')}</Typography>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
placeholder={t('valuePlaceholder')}
|
||||
type='number'
|
||||
value={state || ''}
|
||||
onChange={e => {
|
||||
|
||||
@@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const { t } = useTranslation('things')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [state, setState] = useState(currentThing?.state || '')
|
||||
@@ -13,7 +15,7 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const newErrors = {}
|
||||
|
||||
if (state.trim() === '') {
|
||||
newErrors.state = 'State is required'
|
||||
newErrors.state = t('errStateRequired')
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
@@ -38,18 +40,18 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='sm'
|
||||
title='Update state'
|
||||
title={t('updateState')}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: onClose }}
|
||||
primary={{ label: 'Update', onClick: handleSave }}
|
||||
secondary={{ label: t('common:cancel'), onClick: onClose }}
|
||||
primary={{ label: t('update'), onClick: handleSave }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
<Typography>{t('value')}</Typography>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
placeholder={t('valuePlaceholder')}
|
||||
value={state || ''}
|
||||
onChange={e => setState(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { getTextColorFromBackgroundColor } from '../../../utils/Colors'
|
||||
import PROJECT_ICONS from '../../../utils/ProjectIcons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const IconPickerModal = ({
|
||||
isOpen,
|
||||
@@ -11,6 +12,7 @@ const IconPickerModal = ({
|
||||
currentIcon,
|
||||
projectColor,
|
||||
}) => {
|
||||
const { t } = useTranslation('projects')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const handleIconClick = iconValue => {
|
||||
@@ -25,13 +27,13 @@ const IconPickerModal = ({
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
unmountDelay={250}
|
||||
title='Choose Project Icon'
|
||||
title={t('iconPicker.chooseIcon')}
|
||||
footer={
|
||||
<ModalActions secondary={{ label: 'Cancel', onClick: onClose }} />
|
||||
<ModalActions secondary={{ label: t('common:cancel'), onClick: onClose }} />
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<FormLabel>Available Icons</FormLabel>
|
||||
<FormLabel>{t('iconPicker.availableIcons')}</FormLabel>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
@@ -85,7 +87,7 @@ const IconPickerModal = ({
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{iconData.name}
|
||||
{t(`icons.${iconData.key}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocalization } from '../../../contexts/LocalizationContext'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import ConfirmationModal from './ConfirmationModal'
|
||||
|
||||
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const { t } = useTranslation('timer')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
@@ -181,8 +183,8 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Session updated',
|
||||
message: 'Timer session has been updated successfully.',
|
||||
title: t('toast.sessionUpdatedTitle'),
|
||||
message: t('toast.sessionUpdatedMessage'),
|
||||
})
|
||||
refetchTimer()
|
||||
cancelEditingSession(sessionId)
|
||||
@@ -190,15 +192,15 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
},
|
||||
onError: () => {
|
||||
showError({
|
||||
title: 'Failed to update session',
|
||||
message: 'Please try again.',
|
||||
title: t('toast.sessionUpdateFailTitle'),
|
||||
message: t('toast.tryAgain'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error updating session',
|
||||
title: t('toast.sessionUpdateErrorTitle'),
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
@@ -213,15 +215,15 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Session deleted',
|
||||
message: 'Timer session has been deleted successfully.',
|
||||
title: t('toast.sessionDeletedTitle'),
|
||||
message: t('toast.sessionDeletedMessage'),
|
||||
})
|
||||
refetchTimer()
|
||||
onTimerUpdate?.()
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Error deleting session',
|
||||
title: t('toast.sessionDeleteErrorTitle'),
|
||||
message: error.message,
|
||||
})
|
||||
},
|
||||
@@ -235,10 +237,10 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const confirmDeleteSession = sessionId => {
|
||||
setConfirmDeleteConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Timer Session',
|
||||
message: 'Are you sure you want to delete this timer session?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
title: t('toast.deleteConfirmTitle'),
|
||||
message: t('toast.deleteConfirmMessage'),
|
||||
confirmText: t('common:delete'),
|
||||
cancelText: t('common:cancel'),
|
||||
color: 'danger',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed) {
|
||||
@@ -310,23 +312,23 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
tertiary={
|
||||
!loading && timerData && !editingSessions[timerData.id]
|
||||
? {
|
||||
label: 'Delete',
|
||||
label: t('common:delete'),
|
||||
color: 'danger',
|
||||
onClick: () => confirmDeleteSession(timerData.id),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
secondary={{ label: 'Close', onClick: handleClose }}
|
||||
secondary={{ label: t('common:close'), onClick: handleClose }}
|
||||
primary={
|
||||
!loading && timerData
|
||||
? editingSessions[timerData.id]
|
||||
? {
|
||||
label: 'Save',
|
||||
label: t('common:save'),
|
||||
onClick: () => saveSession(timerData.id),
|
||||
loading,
|
||||
}
|
||||
: {
|
||||
label: 'Edit',
|
||||
label: t('common:edit'),
|
||||
startDecorator: <Edit />,
|
||||
onClick: () => startEditingSession(),
|
||||
}
|
||||
@@ -337,13 +339,13 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
>
|
||||
{loading && (
|
||||
<Alert color='neutral' sx={{ mb: 2 }}>
|
||||
Loading timer data...
|
||||
{t('loading')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!loading && !timerData && (
|
||||
<Alert color='warning' sx={{ mb: 2 }}>
|
||||
No timer data found for this chore.
|
||||
{t('noData')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -403,7 +405,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Active Work
|
||||
{t('activeWork')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -457,7 +459,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Break Time
|
||||
{t('breakTime')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -511,7 +513,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Work Sessions
|
||||
{t('workSessions')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -565,7 +567,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Total Time
|
||||
{t('totalTime')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -597,12 +599,12 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.secondary', fontWeight: 'medium' }}
|
||||
>
|
||||
Work vs Break Distribution
|
||||
{t('distribution')}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
||||
{calculateCurrentActiveDuration() > 0
|
||||
? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active`
|
||||
: 'No active time yet'}
|
||||
: t('noActiveTime')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@@ -630,7 +632,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
{/* Time Session */}
|
||||
<Box>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Session Breakdown
|
||||
{t('sessionBreakdown')}
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
@@ -644,7 +646,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
level='body-sm'
|
||||
sx={{ fontWeight: 'bold', mb: 2 }}
|
||||
>
|
||||
Work Sessions ({timerData.pauseLog.length})
|
||||
{t('workSessions')} ({timerData.pauseLog.length})
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
@@ -795,7 +797,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
level='body-sm'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Sessions
|
||||
{t('sessions')}
|
||||
</Typography>
|
||||
<Button
|
||||
size='sm'
|
||||
@@ -803,7 +805,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
startDecorator={<Add />}
|
||||
onClick={() => addPauseLogEntry(timerData.id)}
|
||||
>
|
||||
Add Session
|
||||
{t('addSession')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -855,7 +857,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
level='body-xs'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Start Time
|
||||
{t('startTime')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
@@ -878,7 +880,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
level='body-xs'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
End Time
|
||||
{t('endTime')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
@@ -903,7 +905,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Leave empty if session is ongoing
|
||||
{t('leaveEmpty')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
@@ -941,7 +943,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
|
||||
{!timerData && (
|
||||
<Alert color='neutral' sx={{ mt: 2 }}>
|
||||
No timer session found for this chore.
|
||||
{t('noSessionForChore')}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import ModalActions from '../../components/common/ModalActions.jsx'
|
||||
@@ -18,6 +19,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal.js'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
|
||||
|
||||
function RedeemPointsModal({ config }) {
|
||||
const { t } = useTranslation('points')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [points, setPoints] = useState(0)
|
||||
@@ -56,12 +58,12 @@ function RedeemPointsModal({ config }) {
|
||||
open={config?.isOpen}
|
||||
onClose={config?.onClose}
|
||||
size='md'
|
||||
title='Redeem Points'
|
||||
title={t('redeem')}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: config?.onClose }}
|
||||
secondary={{ label: t('common:cancel'), onClick: config?.onClose }}
|
||||
primary={{
|
||||
label: 'Redeem',
|
||||
label: t('redeemModal.redeemButton'),
|
||||
startDecorator: <CreditCard />,
|
||||
disabled: !canRedeem,
|
||||
onClick: () =>
|
||||
@@ -103,7 +105,7 @@ function RedeemPointsModal({ config }) {
|
||||
startDecorator={<Toll />}
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
{config?.available || 0} points available
|
||||
{t('redeemModal.pointsAvailable', { count: config?.available || 0 })}
|
||||
</Chip>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -112,7 +114,7 @@ function RedeemPointsModal({ config }) {
|
||||
{/* Points Input Section */}
|
||||
<FormControl>
|
||||
<FormLabel sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Points to Redeem
|
||||
{t('redeemModal.pointsToRedeem')}
|
||||
</FormLabel>
|
||||
<Input
|
||||
type='number'
|
||||
@@ -124,7 +126,7 @@ function RedeemPointsModal({ config }) {
|
||||
input: {
|
||||
min: 0,
|
||||
max: config?.available || 0,
|
||||
placeholder: 'Enter points...',
|
||||
placeholder: t('redeemModal.placeholder'),
|
||||
},
|
||||
}}
|
||||
onChange={e => handlePointsChange(e.target.value)}
|
||||
@@ -140,7 +142,7 @@ function RedeemPointsModal({ config }) {
|
||||
/>
|
||||
{points > config?.available && (
|
||||
<Typography level='body-xs' sx={{ color: 'danger.500', mt: 0.5 }}>
|
||||
Cannot exceed available points
|
||||
{t('redeemModal.cannotExceed')}
|
||||
</Typography>
|
||||
)}
|
||||
</FormControl>
|
||||
@@ -148,7 +150,7 @@ function RedeemPointsModal({ config }) {
|
||||
{/* Quick Selection Buttons */}
|
||||
<Box>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 600, mb: 1.5 }}>
|
||||
Quick Add:
|
||||
{t('redeemModal.quickAdd')}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction='row'
|
||||
@@ -199,19 +201,21 @@ function RedeemPointsModal({ config }) {
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
You are about to redeem
|
||||
{t('redeemModal.aboutToRedeem')}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{ color: 'primary.600', fontWeight: 700 }}
|
||||
>
|
||||
{points} points
|
||||
{t('redeemModal.amount', { count: points })}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.secondary', mt: 0.5 }}
|
||||
>
|
||||
Remaining: {(config?.available || 0) - points} points
|
||||
{t('redeemModal.remaining', {
|
||||
count: (config?.available || 0) - points,
|
||||
})}
|
||||
</Typography>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user