Merge remote-tracking branch 'origin/develop' into analytics
# Conflicts: # public/locales/en/settings.json # src/views/Settings/DeveloperSettings.jsx # src/views/Settings/SettingsOverview.jsx
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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Download } from '@mui/icons-material'
|
||||
import { Box, CircularProgress, Typography } from '@mui/joy'
|
||||
@@ -29,6 +30,7 @@ const downloadUrl = (url, fileName) => {
|
||||
}
|
||||
|
||||
function AttachmentViewerModal({ config }) {
|
||||
const { t } = useTranslation('common')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [imgLoaded, setImgLoaded] = useState(false)
|
||||
const [imgError, setImgError] = useState(false)
|
||||
@@ -49,7 +51,7 @@ function AttachmentViewerModal({ config }) {
|
||||
maxHeight='92vh'
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Close', onClick: handleClose }}
|
||||
secondary={{ label: t('close'), onClick: handleClose }}
|
||||
primary={{
|
||||
label: 'Download',
|
||||
startDecorator: <Download />,
|
||||
@@ -73,7 +75,7 @@ function AttachmentViewerModal({ config }) {
|
||||
)}
|
||||
{imgError ? (
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
Failed to load image.
|
||||
{t('imageLoadFailed')}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box
|
||||
|
||||
@@ -11,12 +11,14 @@ import {
|
||||
Tabs,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { CreateBackup, RestoreBackup } from '../../../utils/Fetcher'
|
||||
|
||||
function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
const { t } = useTranslation('settings')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
@@ -66,7 +68,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
if (!encryptionKey.trim()) {
|
||||
setError('Encryption key is required')
|
||||
setError(t('backup.keyRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Backup created and downloaded successfully',
|
||||
message: t('backup.created'),
|
||||
})
|
||||
|
||||
handleClose()
|
||||
@@ -104,7 +106,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
setError(errorData.message || 'Failed to create backup')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to create backup')
|
||||
setError(t('backup.createFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -120,12 +122,12 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
|
||||
const handleRestore = async () => {
|
||||
if (!restoreEncryptionKey.trim()) {
|
||||
setError('Encryption key is required')
|
||||
setError(t('backup.keyRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!backupFile) {
|
||||
setError('Please select a backup file')
|
||||
setError(t('backup.selectFile'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,7 +144,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
if (response.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Backup restored successfully. Please refresh the page.',
|
||||
message: t('backup.restored'),
|
||||
})
|
||||
|
||||
// Refresh the page after a short delay to allow user to see the message
|
||||
@@ -156,20 +158,20 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
setError(errorData.message || 'Failed to restore backup')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to restore backup')
|
||||
setError(t('backup.restoreFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
setError('Failed to read backup file')
|
||||
setError(t('backup.readFailed'))
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
reader.readAsText(backupFile)
|
||||
} catch (err) {
|
||||
setError('Failed to restore backup')
|
||||
setError(t('backup.restoreFailed'))
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -198,8 +200,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
const renderBackupTab = () => (
|
||||
<Box>
|
||||
<Typography level='body-md' mb={3}>
|
||||
Create an encrypted backup of your data. This backup will include all
|
||||
your chores, history, settings, and optionally your uploaded files.
|
||||
{t('backup.createIntro')}
|
||||
</Typography>
|
||||
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
@@ -208,7 +209,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
type='password'
|
||||
value={encryptionKey}
|
||||
onChange={e => setEncryptionKey(e.target.value)}
|
||||
placeholder='Enter a strong encryption key'
|
||||
placeholder={t('backup.keyPlaceholder')}
|
||||
/>
|
||||
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
||||
Keep this key safe—you'll need it to restore your backup
|
||||
@@ -228,7 +229,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
<Checkbox
|
||||
checked={includeAssets}
|
||||
onChange={e => setIncludeAssets(e.target.checked)}
|
||||
label='Include uploaded files and assets'
|
||||
label={t('backup.includeAssets')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -243,8 +244,8 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
const renderRestoreTab = () => (
|
||||
<Box>
|
||||
<Typography level='body-md' mb={3} color='warning'>
|
||||
<strong>Warning:</strong> Restoring a backup will replace all your
|
||||
current data. This action cannot be undone.
|
||||
<strong>{t('backup.warningLabel')}</strong>{' '}
|
||||
{t('backup.restoreWarning')}
|
||||
</Typography>
|
||||
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
@@ -268,7 +269,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
type='password'
|
||||
value={restoreEncryptionKey}
|
||||
onChange={e => setRestoreEncryptionKey(e.target.value)}
|
||||
placeholder='Enter the encryption key used for this backup'
|
||||
placeholder={t('backup.restoreKeyPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -293,7 +294,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
label: t('accountSettings.cancel'),
|
||||
onClick: handleClose,
|
||||
disabled: loading,
|
||||
}}
|
||||
|
||||
@@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
const { t } = useTranslation('settings')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [childName, setChildName] = useState('')
|
||||
@@ -19,39 +21,38 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
|
||||
if (touched.childName) {
|
||||
if (!childName.trim()) {
|
||||
newErrors.childName = 'Sub account name is required'
|
||||
newErrors.childName = t('childUsers.errNameRequired')
|
||||
} else if (childName.length < 2) {
|
||||
newErrors.childName = 'Sub account name must be at least 2 characters'
|
||||
newErrors.childName = t('childUsers.errNameMin')
|
||||
} else if (childName.length > 20) {
|
||||
newErrors.childName = 'Sub account name must be less than 20 characters'
|
||||
newErrors.childName = t('childUsers.errNameMax')
|
||||
} else if (!/^[a-z.-]+$/.test(childName)) {
|
||||
newErrors.childName =
|
||||
'Sub account name can only contain lowercase letters, dot and dash'
|
||||
newErrors.childName = t('childUsers.errNameChars')
|
||||
}
|
||||
}
|
||||
|
||||
if (touched.password) {
|
||||
if (!password) {
|
||||
newErrors.password = 'Password is required'
|
||||
newErrors.password = t('childUsers.errPasswordRequired')
|
||||
} else if (password.length < 8) {
|
||||
newErrors.password = 'Password must be between 8 and 64 characters'
|
||||
newErrors.password = t('childUsers.errPasswordLength')
|
||||
} else if (password.length > 64) {
|
||||
newErrors.password = 'Password must be between 8 and 64 characters'
|
||||
newErrors.password = t('childUsers.errPasswordLength')
|
||||
}
|
||||
}
|
||||
|
||||
if (touched.confirmPassword) {
|
||||
if (password !== confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match'
|
||||
newErrors.confirmPassword = t('childUsers.errPasswordMatch')
|
||||
}
|
||||
}
|
||||
|
||||
if (touched.displayName && displayName.length > 50) {
|
||||
newErrors.displayName = 'Display name must be less than 50 characters'
|
||||
newErrors.displayName = t('childUsers.errDisplayNameMax')
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
}, [childName, displayName, password, confirmPassword, touched])
|
||||
}, [childName, displayName, password, confirmPassword, touched, t])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setTouched({
|
||||
@@ -101,7 +102,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
title='Create Sub Account'
|
||||
title={t('childUsers.createTitle')}
|
||||
description='Create a login that can complete tasks assigned to this account.'
|
||||
size='md'
|
||||
closeOnBackdrop={!isSubmitting}
|
||||
@@ -109,12 +110,12 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
label: t('accountSettings.cancel'),
|
||||
onClick: handleClose,
|
||||
disabled: isSubmitting,
|
||||
}}
|
||||
primary={{
|
||||
label: 'Create Account',
|
||||
label: t('childUsers.createButton'),
|
||||
onClick: handleSubmit,
|
||||
disabled: !isValid || isSubmitting,
|
||||
loading: isSubmitting,
|
||||
@@ -124,14 +125,14 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
>
|
||||
<FormControl error={!!errors.childName} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Sub Account Name *
|
||||
{t('childUsers.nameLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
required
|
||||
fullWidth
|
||||
id='childName'
|
||||
name='childName'
|
||||
placeholder='Enter sub account name (e.g., sarah)'
|
||||
placeholder={t('childUsers.namePlaceholder')}
|
||||
value={childName}
|
||||
onChange={e => {
|
||||
setChildName(e.target.value)
|
||||
@@ -145,13 +146,13 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
|
||||
<FormControl error={!!errors.displayName} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Display Name
|
||||
{t('childUsers.displayNameLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
fullWidth
|
||||
id='displayName'
|
||||
name='displayName'
|
||||
placeholder='Display name (optional, defaults to sub account name)'
|
||||
placeholder={t('childUsers.displayNamePlaceholder')}
|
||||
value={displayName}
|
||||
onChange={e => {
|
||||
setDisplayName(e.target.value)
|
||||
@@ -165,7 +166,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
|
||||
<FormControl error={!!errors.password} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Password *
|
||||
{t('childUsers.passwordLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
required
|
||||
@@ -173,7 +174,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
name='password'
|
||||
type='password'
|
||||
id='password'
|
||||
placeholder='Enter password (8-64 characters)'
|
||||
placeholder={t('childUsers.passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={e => {
|
||||
setPassword(e.target.value)
|
||||
@@ -185,7 +186,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
|
||||
<FormControl error={!!errors.confirmPassword} sx={{ mb: 3 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Confirm Password *
|
||||
{t('childUsers.confirmPasswordLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
required
|
||||
@@ -193,7 +194,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
name='confirmPassword'
|
||||
type='password'
|
||||
id='confirmPassword'
|
||||
placeholder='Confirm password'
|
||||
placeholder={t('childUsers.confirmPasswordPlaceholder')}
|
||||
value={confirmPassword}
|
||||
onChange={e => {
|
||||
setConfirmPassword(e.target.value)
|
||||
|
||||
@@ -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 { Input } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
const { t } = useTranslation('common')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [date, setDate] = useState(
|
||||
current ? new Date(current).toISOString().split('T')[0] : '',
|
||||
@@ -22,8 +24,8 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
title={title}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: onClose }}
|
||||
primary={{ label: 'Save', onClick: handleSave, disabled: !date }}
|
||||
secondary={{ label: t('cancel'), onClick: onClose }}
|
||||
primary={{ label: t('save'), onClick: handleSave, disabled: !date }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function PasswordChangeModal({ isOpen, onClose }) {
|
||||
const { t } = useTranslation('settings')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
@@ -17,13 +19,13 @@ function PasswordChangeModal({ isOpen, onClose }) {
|
||||
if (password !== confirmPassword) {
|
||||
setPasswordError('Passwords do not match')
|
||||
} else if (password.length < 8) {
|
||||
setPasswordError('Password must be at least 8 characters')
|
||||
setPasswordError(t('passwordChange.minLength'))
|
||||
} else if (password.length > 64) {
|
||||
setPasswordError('Password must be less than 64 characters')
|
||||
setPasswordError(t('passwordChange.maxLength'))
|
||||
} else {
|
||||
setPasswordError(null)
|
||||
}
|
||||
}, [password, confirmPassword, passwordTouched, confirmPasswordTouched])
|
||||
}, [password, confirmPassword, passwordTouched, confirmPasswordTouched, t])
|
||||
|
||||
const handleAction = isConfirmed => onClose(isConfirmed ? password : null)
|
||||
const canSubmit =
|
||||
@@ -38,13 +40,13 @@ function PasswordChangeModal({ isOpen, onClose }) {
|
||||
open={isOpen}
|
||||
onClose={() => handleAction(false)}
|
||||
size='sm'
|
||||
title='Change Password'
|
||||
title={t('accountSettings.changePassword')}
|
||||
description='Choose a password between 8 and 64 characters.'
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: () => handleAction(false) }}
|
||||
secondary={{ label: t('accountSettings.cancel'), onClick: () => handleAction(false) }}
|
||||
primary={{
|
||||
label: 'Change Password',
|
||||
label: t('accountSettings.changePassword'),
|
||||
disabled: !canSubmit,
|
||||
onClick: () => handleAction(true),
|
||||
}}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Textarea } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function TextModal({
|
||||
isOpen,
|
||||
@@ -12,6 +13,7 @@ function TextModal({
|
||||
okText,
|
||||
cancelText,
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [text, setText] = useState(current)
|
||||
|
||||
@@ -35,7 +37,7 @@ function TextModal({
|
||||
>
|
||||
<Textarea
|
||||
autoFocus
|
||||
placeholder='Type in here…'
|
||||
placeholder={t('typeHere')}
|
||||
value={text}
|
||||
onChange={event => setText(event.target.value)}
|
||||
minRows={3}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,8 +14,10 @@ import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { CheckUserDeletion, DeleteUser } from '../../../utils/Fetcher'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function UserDeletionModal({ isOpen, onClose }) {
|
||||
const { t } = useTranslation('settings')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const Navigate = useNavigate()
|
||||
const [step, setStep] = useState(1) // 1: Warning, 2: Transfer, 3: Confirm
|
||||
@@ -47,7 +49,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
|
||||
const checkDeletionRequirements = async () => {
|
||||
if (password.trim() === '') {
|
||||
setError('Please enter your password to continue')
|
||||
setError(t('deletion.passwordRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,7 +99,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
|
||||
const executeUserDeletion = async () => {
|
||||
if (password.trim() === '' || confirmation !== 'DELETE') {
|
||||
setError('Please enter your password and type DELETE to confirm')
|
||||
setError(t('deletion.passwordAndDelete'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +122,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete account:', err)
|
||||
setError('Failed to delete account')
|
||||
setError(t('deletion.failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -181,7 +183,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder='Enter your password'
|
||||
placeholder={t('deletion.passwordPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -196,7 +198,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
const renderTransferStep = () => (
|
||||
<>
|
||||
<Typography level='body-md' mb={3}>
|
||||
You own circles that require ownership transfer before deletion. Please
|
||||
{t('deletion.transferIntro')}
|
||||
select new owners:
|
||||
</Typography>
|
||||
|
||||
@@ -208,7 +210,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
<FormControl>
|
||||
<FormLabel>New Owner</FormLabel>
|
||||
<Select
|
||||
placeholder='Select new owner'
|
||||
placeholder={t('deletion.selectOwner')}
|
||||
value={
|
||||
transferOptions.find(t => t.circleId === circle.id)
|
||||
?.newOwnerId || ''
|
||||
@@ -237,8 +239,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
const renderConfirmationStep = () => (
|
||||
<>
|
||||
<Typography level='body-md' mb={3}>
|
||||
Please enter your password and type <strong>DELETE</strong> to confirm
|
||||
account deletion.
|
||||
{t('deletion.confirmPrompt')}
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2}>
|
||||
on successful deletion, you will be logged out and redirected to the
|
||||
@@ -251,7 +252,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder='Enter your password'
|
||||
placeholder={t('deletion.passwordPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -260,7 +261,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
<Input
|
||||
value={confirmation}
|
||||
onChange={e => setConfirmation(e.target.value)}
|
||||
placeholder='DELETE'
|
||||
placeholder={t('deletion.typeDelete')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -304,7 +305,7 @@ function UserDeletionModal({ isOpen, onClose }) {
|
||||
<ModalActions
|
||||
stackOnMobile
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
label: t('accountSettings.cancel'),
|
||||
onClick: () => handleClose(false),
|
||||
}}
|
||||
primary={{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Avatar, Box, List, ListItem, Typography } from '@mui/joy'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
return (
|
||||
@@ -13,7 +15,7 @@ const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
fullWidth={true}
|
||||
title='Select User'
|
||||
footer={
|
||||
<ModalActions secondary={{ label: 'Cancel', onClick: onClose }} />
|
||||
<ModalActions secondary={{ label: t('cancel'), onClick: onClose }} />
|
||||
}
|
||||
>
|
||||
<List sx={{ mb: 2 }}>
|
||||
|
||||
@@ -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