feat: add i18n support to Settings and AddTaskModal components
- Integrated translation functionality using react-i18next in Settings.jsx - Replaced hardcoded strings with translation keys for better localization support - Updated AddTaskModal.jsx to include translations for task-related strings - Enhanced user experience by providing localized messages for various actions and notifications
This commit is contained in:
@@ -420,5 +420,138 @@
|
||||
"dueAlert": "Due Alert",
|
||||
"followUp": "Follow-up",
|
||||
"rememberFuture": "Remember for Future Tasks"
|
||||
},
|
||||
"choreEdit": {
|
||||
"errNameRequired": "Name is required",
|
||||
"errFrequencyInvalid": "Invalid frequency, the {{unit}} should be > 0",
|
||||
"errSelectDayOfWeek": "Please select at least one day of the week",
|
||||
"errSelectDayOccurrence": "Please select at least one day occurrence for the month",
|
||||
"errSelectMonth": "Please select at least one month",
|
||||
"errStartDateRequired": "Start date is required",
|
||||
"errDueDateRequired": "Due date is required",
|
||||
"errThingTrigger": "Thing trigger is invalid",
|
||||
"errTitle": "Please resolve the following errors:",
|
||||
"savedOfflineTitle": "Saved Offline",
|
||||
"savedOfflineMessage": "Your changes will sync when you are back online.",
|
||||
"savedTitle": "Chore Saved",
|
||||
"savedMessage": "Your task has been saved successfully!",
|
||||
"saveFailedTitle": "Save Failed",
|
||||
"saveFailedMessage": "Failed to save chore, please try again.",
|
||||
"uploadFailedTitle": "Upload Failed",
|
||||
"uploadFailedMessage": "Failed to upload attachment.",
|
||||
"scanFailedTitle": "Scan Failed",
|
||||
"scanFailedMessage": "Could not scan the document.",
|
||||
"scanReadFailedMessage": "Could not read the scanned image.",
|
||||
"deleteFailedTitle": "Delete Failed",
|
||||
"deleteAttachmentFailed": "Failed to delete attachment.",
|
||||
"deleteChoreFailed": "Failed to delete chore: {{error}}",
|
||||
"deleteChoreTitle": "Delete Chore",
|
||||
"name": "Name",
|
||||
"nameDesc": "What is the name of this task?",
|
||||
"description": "Description",
|
||||
"descriptionDesc": "What is this task about?",
|
||||
"priority": "Priority",
|
||||
"priorityDesc": "How important is this task?",
|
||||
"noPriority": "No Priority",
|
||||
"project": "Project",
|
||||
"projectDesc": "Which project does this task belong to?",
|
||||
"defaultProject": "Default Project",
|
||||
"labels": "Labels",
|
||||
"labelsDesc": "Things to remember about this task or to tag it",
|
||||
"addNewLabel": "Add New Label",
|
||||
"subTasks": "Sub Tasks",
|
||||
"attachments": "Attachments",
|
||||
"attachmentsDesc": "Files attached to this task",
|
||||
"uploadFile": "Upload File",
|
||||
"scan": "Scan",
|
||||
"assignees": "Assignees",
|
||||
"assigneesDesc": "Who can do this task?",
|
||||
"anyone": "Anyone",
|
||||
"rememberFuture": "Remember for Future Tasks",
|
||||
"currentlyAssigned": "Currently Assigned To",
|
||||
"currentlyAssignedDesc": "Who is assigned the next due?",
|
||||
"noAssigneesPlaceholder": "No Assignees yet can perform this task",
|
||||
"selectAssigneePlaceholder": "Select an assignee for this task",
|
||||
"assignStrategy": "Assignment Strategy",
|
||||
"assignStrategyDesc": "How to pick the next assignee for the following task?",
|
||||
"strategy": {
|
||||
"random": "Random",
|
||||
"least_assigned": "Least Assigned",
|
||||
"least_completed": "Least Completed",
|
||||
"keep_last_assigned": "Keep Last Assigned",
|
||||
"random_except_last_assigned": "Random Except Last Assigned",
|
||||
"round_robin": "Round Robin",
|
||||
"no_assignee": "No Assignee"
|
||||
},
|
||||
"startDate": "Start Date",
|
||||
"dueDate": "Due Date",
|
||||
"triggerDueHint": "Due Date will be set when the trigger of the thing is met",
|
||||
"giveDueDate": "Give this task a due date",
|
||||
"giveDueDateHelp": "Task needs to be completed by a specific time",
|
||||
"startWhen": "When does this task start?",
|
||||
"dueWhen": "When is the next first time this task is due?",
|
||||
"setSpecificTime": "Set a specific time",
|
||||
"dueAtSpecifiedTime": "Task will be due at the specified time",
|
||||
"dueEndOfDay": "Task will be due at the end of the day (11:59 PM)",
|
||||
"timeLabel": "Time:",
|
||||
"taskWindow": "Task Window",
|
||||
"taskWindowDesc": "Define when this task can be completed and when it expires",
|
||||
"earliestCompletion": "Set earliest completion time",
|
||||
"earliestCompletionHelp": "Task becomes available to complete X hours before the due date",
|
||||
"hoursLabel": "Hours:",
|
||||
"hoursPlaceholder": "Hours",
|
||||
"afterDueDate": "after due date",
|
||||
"schedulingPrefs": "Scheduling Preferences",
|
||||
"schedulingPrefsDesc": "How to reschedule the next due date?",
|
||||
"rescheduleFromDue": "Reschedule from due date",
|
||||
"rescheduleFromDueHelp": "the next task will be scheduled from the original due date, even if the previous task was completed late",
|
||||
"rescheduleFromCompletion": "Reschedule from completion date",
|
||||
"rescheduleFromCompletionHelp": "the next task will be scheduled from the actual completion date of the previous task",
|
||||
"notifications": "Notifications",
|
||||
"notificationsPlanWarning": "Task notifications are not available in the Basic plan. Upgrade to Plus to receive reminders when tasks are due or completed.",
|
||||
"notifyForTask": "Notify for this task",
|
||||
"notifyForTaskHelp": "When should receive notifications for this task",
|
||||
"notificationSchedule": "Notification Schedule",
|
||||
"whoToNotify": "Who to Notify",
|
||||
"allAssignees": "All Assignees",
|
||||
"allAssigneesHelp": "Notify all assignees",
|
||||
"specificGroup": "Specific Group",
|
||||
"specificGroupHelp": "Notify a specific group",
|
||||
"telegramGroupIdLabel": "Telegram Group ID:",
|
||||
"telegramGroupIdPlaceholder": "Telegram Group ID",
|
||||
"taskSettings": "Task Settings:",
|
||||
"pointsSystem": "Points System",
|
||||
"assignPoints": "Assign points for completion",
|
||||
"assignPointsHelp": "Assign points to this task and user will earn points when they completed it",
|
||||
"pointsLabel": "Points:",
|
||||
"pointsPlaceholder": "Points",
|
||||
"approvalRequirement": "Approval Requirement",
|
||||
"requireApproval": "Require admin approval",
|
||||
"requireApprovalHelp": "This task will need approval from an admin before being marked as complete",
|
||||
"privacySettings": "Privacy Settings",
|
||||
"privacyDesc": "Who can see this task?",
|
||||
"public": "Public",
|
||||
"publicHelp": "Everyone in your circle",
|
||||
"limited": "Limited",
|
||||
"limitedHelp": "You and others that are assigned to the task",
|
||||
"limitedDisabledHint": " (No assignees selected, Limited option is disabled)",
|
||||
"createdBy": "Created by",
|
||||
"updatedBy": "Updated by",
|
||||
"archive": "Archive",
|
||||
"unarchive": "Unarchive",
|
||||
"create": "Create"
|
||||
},
|
||||
"addTask": {
|
||||
"title": "Create new task",
|
||||
"defaultProject": "Default Project",
|
||||
"anyone": "Anyone",
|
||||
"taskPlaceholder": "Type your task...",
|
||||
"descriptionChip": "Description",
|
||||
"subtasksChip": "Subtasks",
|
||||
"creating": "Creating…",
|
||||
"createCount": "Create {{count}} Tasks",
|
||||
"useTask": "Use Task",
|
||||
"processing": "Processing",
|
||||
"create": "Create"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,5 +243,90 @@
|
||||
"cancelAll": "Cancel All"
|
||||
},
|
||||
"attachments": "Attachments",
|
||||
"fileNumbered": "File {{index}}"
|
||||
"fileNumbered": "File {{index}}",
|
||||
"onboarding": {
|
||||
"sources": {
|
||||
"speak": "Speak",
|
||||
"snap": "Snap",
|
||||
"type": "Type"
|
||||
},
|
||||
"capture": {
|
||||
"voiceCaption": "Dates, labels and points from your words",
|
||||
"deviceCaption": "Read on your device",
|
||||
"typeCaption": "#labels @people *points as you type",
|
||||
"trashTitle": "♻️ Take out the trash",
|
||||
"trashDue": "Due tomorrow",
|
||||
"trashRepeat": "Every Monday",
|
||||
"homeLabel": "Home",
|
||||
"vehicleTitle": "🚗 Vehicle Registration Renewal",
|
||||
"vehicleDue": "Due Aug 3",
|
||||
"acTitle": "💨 Change the AC filter",
|
||||
"acDue": "Due Fri",
|
||||
"acRepeat": "Every 3 months"
|
||||
},
|
||||
"schedule": {
|
||||
"fromDueDate": "Reschedule from due date",
|
||||
"fromCompletionDate": "Reschedule from completion date",
|
||||
"due": "Due",
|
||||
"done": "Done",
|
||||
"next": "Next"
|
||||
},
|
||||
"nfc": {
|
||||
"tagOnWasher": "Tag on the washer",
|
||||
"tapToOpen": "Tap to open",
|
||||
"swapFilter": "Swap the washer filter",
|
||||
"done": "Done",
|
||||
"skip": "Skip",
|
||||
"startTimer": "Start timer",
|
||||
"waitingApproval": "{{name}} marked it done · waiting on you",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"completed": "Completed",
|
||||
"note": "Used the delicate cycle — filter needs a clean next time.",
|
||||
"metaJustNow": "Just now · ★ 5 pts",
|
||||
"metaLastWeek": "Last week · ★ 5 pts"
|
||||
},
|
||||
"circle": {
|
||||
"kitchenTitle": "Kitchen deep clean",
|
||||
"kitchenDue": "Due Sat",
|
||||
"kitchenRepeat": "Every week",
|
||||
"takesTurns": "Takes turns"
|
||||
},
|
||||
"turns": {
|
||||
"binsTitle": "🗑️ Take bins to the curb",
|
||||
"binsDue": "Due Tomorrow",
|
||||
"binsRepeat": "Every week",
|
||||
"completed": "Completed",
|
||||
"metaJustNow": "Just now · ★ 5 pts"
|
||||
},
|
||||
"widget": {
|
||||
"schoolForms": "School forms",
|
||||
"takeOutTrash": "Take out the trash",
|
||||
"waterPlants": "Water the plants",
|
||||
"today": "Today",
|
||||
"left": "{{count}} left"
|
||||
},
|
||||
"reminders": {
|
||||
"trashTitle": "Take out the trash",
|
||||
"trashBody": "Due in 30 minutes · Bin night",
|
||||
"finishedTitle": "Amalie finished Kitchen deep clean",
|
||||
"finishedBody": "Your turn is next Saturday"
|
||||
},
|
||||
"things": {
|
||||
"waterBill": "Water bill",
|
||||
"acFilter": "AC filter",
|
||||
"trashDay": "Trash day",
|
||||
"dogMedicine": "Dog's medicine",
|
||||
"whoseTurn": "Whose turn to cook",
|
||||
"monthly": "Monthly",
|
||||
"everyThreeMonths": "Every 3 months",
|
||||
"everyMonday": "Every Monday",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"tagBills": "Bills",
|
||||
"tagHome": "Home",
|
||||
"tagPets": "Pets",
|
||||
"tagCooking": "Cooking"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,8 @@
|
||||
"joinCircle": "Join Circle",
|
||||
"joinedPending": "Joined circle successfully, wait for the circle owner to accept your request.",
|
||||
"alreadyMember": "You are already a member of this circle",
|
||||
"joinFailed": "Failed to join circle"
|
||||
"joinFailed": "Failed to join circle",
|
||||
"copyLink": "Copy Link"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Account Settings",
|
||||
@@ -423,7 +424,8 @@
|
||||
"name": "Weekly Goals",
|
||||
"description": "Shows weekly progress and family completion stats"
|
||||
}
|
||||
}
|
||||
},
|
||||
"detailedDescription": "Customize the layout and visibility of cards in the sidepanel. the section only available on large screen devices such as tablets and desktops.."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Preferences",
|
||||
@@ -457,7 +459,8 @@
|
||||
"ymd": "YYYY-MM-DD (ISO)",
|
||||
"long": "Long format (e.g., January 1, 2024)",
|
||||
"short": "Short format (e.g., Jan 1, 2024)"
|
||||
}
|
||||
},
|
||||
"descriptionLong": "Customize language, date format, and regional preferences for your account. These settings will apply throughout the application."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced Settings",
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import DurationInput from '../../components/common/DurationInput'
|
||||
@@ -94,6 +95,7 @@ const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
|
||||
const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once']
|
||||
const NO_DUE_DATE_ALLOWED_TYPE = ['trigger']
|
||||
const ChoreEdit = () => {
|
||||
const { t } = useTranslation('chores')
|
||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||
useUserProfile()
|
||||
|
||||
@@ -207,16 +209,18 @@ const ChoreEdit = () => {
|
||||
const errors = {}
|
||||
|
||||
if (name.trim() === '') {
|
||||
errors.name = 'Name is required'
|
||||
errors.name = t('choreEdit.errNameRequired')
|
||||
}
|
||||
if (frequencyType === 'interval' && !frequency > 0) {
|
||||
errors.frequency = `Invalid frequency, the ${frequencyMetadata.unit} should be > 0`
|
||||
errors.frequency = t('choreEdit.errFrequencyInvalid', {
|
||||
unit: frequencyMetadata.unit,
|
||||
})
|
||||
}
|
||||
if (
|
||||
frequencyType === 'days_of_the_week' &&
|
||||
frequencyMetadata['days']?.length === 0
|
||||
) {
|
||||
errors.frequency = 'Please select at least one day of the week'
|
||||
errors.frequency = t('choreEdit.errSelectDayOfWeek')
|
||||
}
|
||||
|
||||
// Validate advanced scheduling patterns
|
||||
@@ -226,14 +230,13 @@ const ChoreEdit = () => {
|
||||
(!frequencyMetadata?.occurrences ||
|
||||
frequencyMetadata.occurrences.length === 0)
|
||||
) {
|
||||
errors.frequency =
|
||||
'Please select at least one day occurrence for the month'
|
||||
errors.frequency = t('choreEdit.errSelectDayOccurrence')
|
||||
}
|
||||
if (
|
||||
frequencyType === 'day_of_the_month' &&
|
||||
frequencyMetadata['months']?.length === 0
|
||||
) {
|
||||
errors.frequency = 'Please select at least one month'
|
||||
errors.frequency = t('choreEdit.errSelectMonth')
|
||||
}
|
||||
if (
|
||||
dueDate === null &&
|
||||
@@ -243,14 +246,14 @@ const ChoreEdit = () => {
|
||||
if (REPEAT_ON_TYPE.includes(frequencyType)) {
|
||||
console.log('VALIDATION:', dueDate, frequencyType)
|
||||
|
||||
errors.dueDate = 'Start date is required'
|
||||
errors.dueDate = t('choreEdit.errStartDateRequired')
|
||||
} else {
|
||||
errors.dueDate = 'Due date is required'
|
||||
errors.dueDate = t('choreEdit.errDueDateRequired')
|
||||
}
|
||||
}
|
||||
if (frequencyType === 'trigger') {
|
||||
if (!isThingValid) {
|
||||
errors.thingTrigger = 'Thing trigger is invalid'
|
||||
errors.thingTrigger = t('choreEdit.errThingTrigger')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +266,7 @@ const ChoreEdit = () => {
|
||||
<ListItem key={key}>{errors[key]}</ListItem>
|
||||
))
|
||||
showError({
|
||||
title: 'Please resolve the following errors:',
|
||||
title: t('choreEdit.errTitle'),
|
||||
message: <List>{errorList}</List>,
|
||||
})
|
||||
return false
|
||||
@@ -417,13 +420,13 @@ const ChoreEdit = () => {
|
||||
result?.res?._pendingCreate
|
||||
) {
|
||||
showSuccess({
|
||||
title: 'Saved Offline',
|
||||
message: 'Your changes will sync when you are back online.',
|
||||
title: t('choreEdit.savedOfflineTitle'),
|
||||
message: t('choreEdit.savedOfflineMessage'),
|
||||
})
|
||||
} else {
|
||||
showSuccess({
|
||||
title: 'Chore Saved',
|
||||
message: 'Your task has been saved successfully!',
|
||||
title: t('choreEdit.savedTitle'),
|
||||
message: t('choreEdit.savedMessage'),
|
||||
})
|
||||
}
|
||||
Navigate('/chores')
|
||||
@@ -431,10 +434,10 @@ const ChoreEdit = () => {
|
||||
.catch(error => {
|
||||
console.error('Failed to save chore:', error)
|
||||
showError({
|
||||
title: 'Save Failed',
|
||||
title: t('choreEdit.saveFailedTitle'),
|
||||
message: error?.isServerMessage
|
||||
? error.message
|
||||
: 'Failed to save chore, please try again.',
|
||||
: t('choreEdit.saveFailedMessage'),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -693,8 +696,8 @@ const ChoreEdit = () => {
|
||||
})
|
||||
if (!response.ok) {
|
||||
showError({
|
||||
title: 'Upload Failed',
|
||||
message: 'Failed to upload attachment.',
|
||||
title: t('choreEdit.uploadFailedTitle'),
|
||||
message: t('choreEdit.uploadFailedMessage'),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -710,8 +713,8 @@ const ChoreEdit = () => {
|
||||
])
|
||||
} catch {
|
||||
showError({
|
||||
title: 'Upload Failed',
|
||||
message: 'Failed to upload attachment.',
|
||||
title: t('choreEdit.uploadFailedTitle'),
|
||||
message: t('choreEdit.uploadFailedMessage'),
|
||||
})
|
||||
} finally {
|
||||
setIsUploadingAttachment(false)
|
||||
@@ -725,16 +728,16 @@ const ChoreEdit = () => {
|
||||
if (cancelled) return
|
||||
if (error || !image) {
|
||||
showError({
|
||||
title: 'Scan Failed',
|
||||
message: error || 'Could not scan the document.',
|
||||
title: t('choreEdit.scanFailedTitle'),
|
||||
message: error || t('choreEdit.scanFailedMessage'),
|
||||
})
|
||||
return
|
||||
}
|
||||
const file = await imageSourceToFile(image, `scan-${Date.now()}.jpg`)
|
||||
if (!file) {
|
||||
showError({
|
||||
title: 'Scan Failed',
|
||||
message: 'Could not read the scanned image.',
|
||||
title: t('choreEdit.scanFailedTitle'),
|
||||
message: t('choreEdit.scanReadFailedMessage'),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -744,10 +747,10 @@ const ChoreEdit = () => {
|
||||
const handleDelete = () => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Chore',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
message: 'Are you sure you want to delete this chore?',
|
||||
title: t('choreEdit.deleteChoreTitle'),
|
||||
confirmText: t('common:delete'),
|
||||
cancelText: t('common:cancel'),
|
||||
message: t('edit.deleteConfirm'),
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
deleteChores.mutate([choreId], {
|
||||
@@ -756,8 +759,10 @@ const ChoreEdit = () => {
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Delete Failed',
|
||||
message: `Failed to delete chore: ${error.message}`,
|
||||
title: t('choreEdit.deleteFailedTitle'),
|
||||
message: t('choreEdit.deleteChoreFailed', {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -789,10 +794,8 @@ const ChoreEdit = () => {
|
||||
|
||||
<Box mb={3}>
|
||||
<FormControl error={errors.name}>
|
||||
<Typography level='h4'>Name</Typography>
|
||||
<Typography level='body-md'>
|
||||
What is the name of this task?
|
||||
</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.name')}</Typography>
|
||||
<Typography level='body-md'>{t('choreEdit.nameDesc')}</Typography>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} />
|
||||
<FormHelperText error>{errors.name}</FormHelperText>
|
||||
</FormControl>
|
||||
@@ -800,8 +803,10 @@ const ChoreEdit = () => {
|
||||
|
||||
<Box mb={3}>
|
||||
<FormControl error={errors.description}>
|
||||
<Typography level='h4'>Description</Typography>
|
||||
<Typography level='body-md'>What is this task about?</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.description')}</Typography>
|
||||
<Typography level='body-md'>
|
||||
{t('choreEdit.descriptionDesc')}
|
||||
</Typography>
|
||||
<RichTextEditor
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
@@ -814,8 +819,8 @@ const ChoreEdit = () => {
|
||||
</Box>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Priority</Typography>
|
||||
<Typography level='body-md'>How important is this task?</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.priority')}</Typography>
|
||||
<Typography level='body-md'>{t('choreEdit.priorityDesc')}</Typography>
|
||||
|
||||
{/* Priority Chip Selection */}
|
||||
<Box
|
||||
@@ -861,7 +866,7 @@ const ChoreEdit = () => {
|
||||
minHeight: 34,
|
||||
}}
|
||||
>
|
||||
No Priority
|
||||
{t('choreEdit.noPriority')}
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -869,9 +874,9 @@ const ChoreEdit = () => {
|
||||
{/* Project Selection - Show only if there are multiple projects */}
|
||||
{projects.length >= 1 && (
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Project</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.project')}</Typography>
|
||||
<Typography level='body-md'>
|
||||
Which project does this task belong to?
|
||||
{t('choreEdit.projectDesc')}
|
||||
</Typography>
|
||||
<Select
|
||||
value={projectId}
|
||||
@@ -906,7 +911,7 @@ const ChoreEdit = () => {
|
||||
)
|
||||
})()}
|
||||
</Avatar>
|
||||
Default Project
|
||||
{t('choreEdit.defaultProject')}
|
||||
</Box>
|
||||
</Option>
|
||||
{projects.map(project => (
|
||||
@@ -947,10 +952,8 @@ const ChoreEdit = () => {
|
||||
)}
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Labels</Typography>
|
||||
<Typography level='body-md'>
|
||||
Things to remember about this task or to tag it
|
||||
</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.labels')}</Typography>
|
||||
<Typography level='body-md'>{t('choreEdit.labelsDesc')}</Typography>
|
||||
<Select
|
||||
multiple
|
||||
onChange={(event, newValue) => {
|
||||
@@ -1010,13 +1013,13 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
>
|
||||
<Add />
|
||||
Add New Label
|
||||
{t('choreEdit.addNewLabel')}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography level='h4'>Sub Tasks</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.subTasks')}</Typography>
|
||||
{/* <FormControl sx={{ mt: 1 }}>
|
||||
<Checkbox
|
||||
onChange={e => {
|
||||
@@ -1049,8 +1052,10 @@ const ChoreEdit = () => {
|
||||
</Box>
|
||||
|
||||
<Box mt={3}>
|
||||
<Typography level='h4'>Attachments</Typography>
|
||||
<Typography level='body-md'>Files attached to this task</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.attachments')}</Typography>
|
||||
<Typography level='body-md'>
|
||||
{t('choreEdit.attachmentsDesc')}
|
||||
</Typography>
|
||||
<Card variant='outlined' sx={{ mt: 2, p: 1.5 }}>
|
||||
{attachments.length > 0 && (
|
||||
<Box
|
||||
@@ -1140,8 +1145,8 @@ const ChoreEdit = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
showError({
|
||||
title: 'Delete Failed',
|
||||
message: 'Failed to delete attachment.',
|
||||
title: t('choreEdit.deleteFailedTitle'),
|
||||
message: t('choreEdit.deleteAttachmentFailed'),
|
||||
})
|
||||
})
|
||||
}}
|
||||
@@ -1166,8 +1171,8 @@ const ChoreEdit = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
showError({
|
||||
title: 'Delete Failed',
|
||||
message: 'Failed to delete attachment.',
|
||||
title: t('choreEdit.deleteFailedTitle'),
|
||||
message: t('choreEdit.deleteAttachmentFailed'),
|
||||
})
|
||||
})
|
||||
}}
|
||||
@@ -1188,7 +1193,7 @@ const ChoreEdit = () => {
|
||||
startDecorator={isUploadingAttachment ? null : <UploadFile />}
|
||||
loading={isUploadingAttachment}
|
||||
>
|
||||
Upload File
|
||||
{t('choreEdit.uploadFile')}
|
||||
<input
|
||||
type='file'
|
||||
hidden
|
||||
@@ -1208,7 +1213,7 @@ const ChoreEdit = () => {
|
||||
disabled={isUploadingAttachment}
|
||||
onClick={handleScanAttachment}
|
||||
>
|
||||
Scan
|
||||
{t('choreEdit.scan')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -1219,8 +1224,10 @@ const ChoreEdit = () => {
|
||||
{/* Section 2: Assignment & Responsibility */}
|
||||
<Box mb={4}>
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Assignees</Typography>
|
||||
<Typography level='body-md'>Who can do this task?</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.assignees')}</Typography>
|
||||
<Typography level='body-md'>
|
||||
{t('choreEdit.assigneesDesc')}
|
||||
</Typography>
|
||||
<Card>
|
||||
<List
|
||||
orientation='horizontal'
|
||||
@@ -1242,7 +1249,7 @@ const ChoreEdit = () => {
|
||||
overlay
|
||||
disableIcon
|
||||
variant='soft'
|
||||
label='Anyone'
|
||||
label={t('choreEdit.anyone')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
@@ -1308,7 +1315,7 @@ const ChoreEdit = () => {
|
||||
setShowSaveAssigneeDefault(false)
|
||||
}}
|
||||
>
|
||||
Remember for Future Tasks
|
||||
{t('choreEdit.rememberFuture')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -1323,15 +1330,17 @@ const ChoreEdit = () => {
|
||||
assignStrategyValue === 'no_assignee' ? 'none' : 'block',
|
||||
}}
|
||||
>
|
||||
<Typography level='h4'>Currently Assigned To</Typography>
|
||||
<Typography level='h4'>
|
||||
{t('choreEdit.currentlyAssigned')}
|
||||
</Typography>
|
||||
<Typography level='body-md'>
|
||||
Who is assigned the next due?
|
||||
{t('choreEdit.currentlyAssignedDesc')}
|
||||
</Typography>
|
||||
<Select
|
||||
placeholder={
|
||||
assignees.length === 0
|
||||
? 'No Assignees yet can perform this task'
|
||||
: 'Select an assignee for this task'
|
||||
? t('choreEdit.noAssigneesPlaceholder')
|
||||
: t('choreEdit.selectAssigneePlaceholder')
|
||||
}
|
||||
disabled={assignees.length === 0}
|
||||
value={assignedToValue}
|
||||
@@ -1348,9 +1357,11 @@ const ChoreEdit = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography level='h4'>Assignment Strategy</Typography>
|
||||
<Typography level='h4'>
|
||||
{t('choreEdit.assignStrategy')}
|
||||
</Typography>
|
||||
<Typography level='body-md'>
|
||||
How to pick the next assignee for the following task?
|
||||
{t('choreEdit.assignStrategyDesc')}
|
||||
</Typography>
|
||||
<Card>
|
||||
<List
|
||||
@@ -1369,10 +1380,7 @@ const ChoreEdit = () => {
|
||||
overlay
|
||||
disableIcon
|
||||
variant='soft'
|
||||
label={item
|
||||
.split('_')
|
||||
.map(x => x.charAt(0).toUpperCase() + x.slice(1))
|
||||
.join(' ')}
|
||||
label={t(`choreEdit.strategy.${item}`)}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
@@ -1412,11 +1420,13 @@ const ChoreEdit = () => {
|
||||
|
||||
<Box mt={3} mb={3}>
|
||||
<Typography level='h4'>
|
||||
{REPEAT_ON_TYPE.includes(frequencyType) ? 'Start Date' : 'Due Date'}
|
||||
{REPEAT_ON_TYPE.includes(frequencyType)
|
||||
? t('choreEdit.startDate')
|
||||
: t('choreEdit.dueDate')}
|
||||
</Typography>
|
||||
{frequencyType === 'trigger' && !dueDate && (
|
||||
<Typography level='body-sm'>
|
||||
Due Date will be set when the trigger of the thing is met
|
||||
{t('choreEdit.triggerDueHint')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -1442,11 +1452,9 @@ const ChoreEdit = () => {
|
||||
defaultChecked={dueDate !== null}
|
||||
checked={dueDate !== null}
|
||||
overlay
|
||||
label='Give this task a due date'
|
||||
label={t('choreEdit.giveDueDate')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Task needs to be completed by a specific time
|
||||
</FormHelperText>
|
||||
<FormHelperText>{t('choreEdit.giveDueDateHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
{dueDate && (
|
||||
@@ -1454,8 +1462,8 @@ const ChoreEdit = () => {
|
||||
<FormControl error={Boolean(errors.dueDate)} sx={{ mt: 2 }}>
|
||||
<Typography level='body-md'>
|
||||
{REPEAT_ON_TYPE.includes(frequencyType)
|
||||
? 'When does this task start?'
|
||||
: 'When is the next first time this task is due?'}
|
||||
? t('choreEdit.startWhen')
|
||||
: t('choreEdit.dueWhen')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='date'
|
||||
@@ -1471,19 +1479,19 @@ const ChoreEdit = () => {
|
||||
checked={useCustomTime}
|
||||
onChange={e => handleUseCustomTimeChange(e.target.checked)}
|
||||
overlay
|
||||
label='Set a specific time'
|
||||
label={t('choreEdit.setSpecificTime')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
{useCustomTime
|
||||
? 'Task will be due at the specified time'
|
||||
: 'Task will be due at the end of the day (11:59 PM)'}
|
||||
? t('choreEdit.dueAtSpecifiedTime')
|
||||
: t('choreEdit.dueEndOfDay')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{useCustomTime && (
|
||||
<Box sx={{ mt: 2, ml: 4 }}>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
Time:
|
||||
{t('choreEdit.timeLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='time'
|
||||
@@ -1499,9 +1507,9 @@ const ChoreEdit = () => {
|
||||
|
||||
{dueDate && (
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Task Window</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.taskWindow')}</Typography>
|
||||
<Typography level='body-md'>
|
||||
Define when this task can be completed and when it expires
|
||||
{t('choreEdit.taskWindowDesc')}
|
||||
</Typography>
|
||||
|
||||
{/* Available From (Completion Window) */}
|
||||
@@ -1516,10 +1524,10 @@ const ChoreEdit = () => {
|
||||
}
|
||||
}}
|
||||
overlay
|
||||
label='Set earliest completion time'
|
||||
label={t('choreEdit.earliestCompletion')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Task becomes available to complete X hours before the due date
|
||||
{t('choreEdit.earliestCompletionHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
@@ -1531,7 +1539,9 @@ const ChoreEdit = () => {
|
||||
ml: 4,
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Hours:</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{t('choreEdit.hoursLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='number'
|
||||
value={completionWindow}
|
||||
@@ -1542,7 +1552,7 @@ const ChoreEdit = () => {
|
||||
max: 24 * 7,
|
||||
},
|
||||
}}
|
||||
placeholder='Hours'
|
||||
placeholder={t('choreEdit.hoursPlaceholder')}
|
||||
onChange={e => {
|
||||
setCompletionWindow(parseInt(e.target.value))
|
||||
}}
|
||||
@@ -1589,7 +1599,9 @@ const ChoreEdit = () => {
|
||||
size='sm'
|
||||
minValue={0}
|
||||
/>
|
||||
<Typography level='body-sm'>after due date</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{t('choreEdit.afterDueDate')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
@@ -1597,9 +1609,9 @@ const ChoreEdit = () => {
|
||||
|
||||
{!['once', 'no_repeat'].includes(frequencyType) && (
|
||||
<Box>
|
||||
<Typography level='h4'>Scheduling Preferences</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.schedulingPrefs')}</Typography>
|
||||
<Typography level='body-md'>
|
||||
How to reschedule the next due date?
|
||||
{t('choreEdit.schedulingPrefsDesc')}
|
||||
</Typography>
|
||||
<RadioGroup name='tiers' sx={{ gap: 1, '& > div': { p: 1 } }}>
|
||||
<FormControl>
|
||||
@@ -1607,11 +1619,10 @@ const ChoreEdit = () => {
|
||||
overlay
|
||||
checked={!isRolling}
|
||||
onClick={() => setIsRolling(false)}
|
||||
label='Reschedule from due date'
|
||||
label={t('choreEdit.rescheduleFromDue')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
the next task will be scheduled from the original due date,
|
||||
even if the previous task was completed late
|
||||
{t('choreEdit.rescheduleFromDueHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
@@ -1622,11 +1633,10 @@ const ChoreEdit = () => {
|
||||
setIsRolling(true)
|
||||
setDeadlineOffset(-1)
|
||||
}}
|
||||
label='Reschedule from completion date'
|
||||
label={t('choreEdit.rescheduleFromCompletion')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
the next task will be scheduled from the actual completion
|
||||
date of the previous task
|
||||
{t('choreEdit.rescheduleFromCompletionHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</RadioGroup>
|
||||
@@ -1635,11 +1645,10 @@ const ChoreEdit = () => {
|
||||
{/* Section 3.1: Notifications */}
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Notifications</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.notifications')}</Typography>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mb: 1 }}>
|
||||
Task notifications are not available in the Basic plan. Upgrade to
|
||||
Plus to receive reminders when tasks are due or completed.
|
||||
{t('choreEdit.notificationsPlanWarning')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -1655,14 +1664,14 @@ const ChoreEdit = () => {
|
||||
checked={isNotificable}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
overlay
|
||||
label='Notify for this task'
|
||||
label={t('choreEdit.notifyForTask')}
|
||||
/>
|
||||
<FormHelperText
|
||||
sx={{
|
||||
opacity: !isPlusAccount(userProfile) ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
When should receive notifications for this task
|
||||
{t('choreEdit.notifyForTaskHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Box>
|
||||
@@ -1677,7 +1686,7 @@ const ChoreEdit = () => {
|
||||
>
|
||||
<Card variant='outlined'>
|
||||
<Typography level='h4' mb={2}>
|
||||
Notification Schedule
|
||||
{t('choreEdit.notificationSchedule')}
|
||||
</Typography>
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<NotificationTemplate
|
||||
@@ -1695,16 +1704,18 @@ const ChoreEdit = () => {
|
||||
</Box>
|
||||
|
||||
<Typography level='h4' mt={3} mb={2}>
|
||||
Who to Notify
|
||||
{t('choreEdit.whoToNotify')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
overlay
|
||||
disabled={true}
|
||||
checked={true}
|
||||
label='All Assignees'
|
||||
label={t('choreEdit.allAssignees')}
|
||||
/>
|
||||
<FormHelperText>Notify all assignees</FormHelperText>
|
||||
<FormHelperText>
|
||||
{t('choreEdit.allAssigneesHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
@@ -1725,9 +1736,11 @@ const ChoreEdit = () => {
|
||||
? notificationMetadata?.circleGroup
|
||||
: false
|
||||
}
|
||||
label='Specific Group'
|
||||
label={t('choreEdit.specificGroup')}
|
||||
/>
|
||||
<FormHelperText>Notify a specific group</FormHelperText>
|
||||
<FormHelperText>
|
||||
{t('choreEdit.specificGroupHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{notificationMetadata?.circleGroup && (
|
||||
@@ -1737,11 +1750,13 @@ const ChoreEdit = () => {
|
||||
ml: 4,
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Telegram Group ID:</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{t('choreEdit.telegramGroupIdLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='number'
|
||||
value={notificationMetadata?.circleGroupID}
|
||||
placeholder='Telegram Group ID'
|
||||
placeholder={t('choreEdit.telegramGroupIdPlaceholder')}
|
||||
onChange={e => {
|
||||
setNotificationMetadata({
|
||||
...notificationMetadata,
|
||||
@@ -1766,11 +1781,11 @@ const ChoreEdit = () => {
|
||||
pb: 1,
|
||||
}}
|
||||
>
|
||||
Task Settings:
|
||||
{t('choreEdit.taskSettings')}
|
||||
</Typography>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Points System</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.pointsSystem')}</Typography>
|
||||
<FormControl sx={{ mt: 1 }}>
|
||||
<Checkbox
|
||||
onChange={e => {
|
||||
@@ -1782,12 +1797,9 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
checked={points > -1}
|
||||
overlay
|
||||
label='Assign points for completion'
|
||||
label={t('choreEdit.assignPoints')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Assign points to this task and user will earn points when they
|
||||
completed it
|
||||
</FormHelperText>
|
||||
<FormHelperText>{t('choreEdit.assignPointsHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
{points != -1 && (
|
||||
<Card variant='outlined' sx={{ mt: 2 }}>
|
||||
@@ -1797,7 +1809,9 @@ const ChoreEdit = () => {
|
||||
ml: 4,
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Points:</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{t('choreEdit.pointsLabel')}
|
||||
</Typography>
|
||||
<Input
|
||||
type='number'
|
||||
value={points}
|
||||
@@ -1808,7 +1822,7 @@ const ChoreEdit = () => {
|
||||
max: 1000,
|
||||
},
|
||||
}}
|
||||
placeholder='Points'
|
||||
placeholder={t('choreEdit.pointsPlaceholder')}
|
||||
onChange={e => {
|
||||
setPoints(parseInt(e.target.value))
|
||||
}}
|
||||
@@ -1819,7 +1833,9 @@ const ChoreEdit = () => {
|
||||
</Box>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='h4'>Approval Requirement</Typography>
|
||||
<Typography level='h4'>
|
||||
{t('choreEdit.approvalRequirement')}
|
||||
</Typography>
|
||||
<FormControl sx={{ mt: 1 }}>
|
||||
<Checkbox
|
||||
onChange={e => {
|
||||
@@ -1827,18 +1843,17 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
checked={requireApproval}
|
||||
overlay
|
||||
label='Require admin approval'
|
||||
label={t('choreEdit.requireApproval')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
This task will need approval from an admin before being marked as
|
||||
complete
|
||||
{t('choreEdit.requireApprovalHelp')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography level='h4'>Privacy Settings</Typography>
|
||||
<Typography level='body-md'>Who can see this task?</Typography>
|
||||
<Typography level='h4'>{t('choreEdit.privacySettings')}</Typography>
|
||||
<Typography level='body-md'>{t('choreEdit.privacyDesc')}</Typography>
|
||||
<RadioGroup
|
||||
name='isPrivate'
|
||||
value={isPrivate}
|
||||
@@ -1852,20 +1867,20 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<Radio overlay value={false} label='Public' />
|
||||
<FormHelperText>Everyone in your circle</FormHelperText>
|
||||
<Radio overlay value={false} label={t('choreEdit.public')} />
|
||||
<FormHelperText>{t('choreEdit.publicHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Radio
|
||||
overlay
|
||||
disabled={anyone || assignableTo.length === 0}
|
||||
value={true}
|
||||
label='Limited'
|
||||
label={t('choreEdit.limited')}
|
||||
/>
|
||||
<FormHelperText>
|
||||
You and others that are assigned to the task
|
||||
{t('choreEdit.limitedHelp')}
|
||||
{anyone || assignableTo.length === 0
|
||||
? ' (No assignees selected, Limited option is disabled)'
|
||||
? t('choreEdit.limitedDisabledHint')
|
||||
: ''}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
@@ -1893,7 +1908,7 @@ const ChoreEdit = () => {
|
||||
setShowSavePrivacyDefault(false)
|
||||
}}
|
||||
>
|
||||
Remember for Future Tasks
|
||||
{t('choreEdit.rememberFuture')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -1910,7 +1925,7 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
>
|
||||
<Typography level='body1'>
|
||||
Created by{' '}
|
||||
{t('choreEdit.createdBy')}{' '}
|
||||
<Chip variant='solid'>
|
||||
{membersData.res.find(f => f.userId === createdBy)?.displayName}
|
||||
</Chip>{' '}
|
||||
@@ -1921,7 +1936,7 @@ const ChoreEdit = () => {
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
<Typography level='body1'>
|
||||
Updated by{' '}
|
||||
{t('choreEdit.updatedBy')}{' '}
|
||||
<Chip variant='solid'>
|
||||
{
|
||||
membersData.res.find(f => f.userId === updatedBy)
|
||||
@@ -1971,7 +1986,7 @@ const ChoreEdit = () => {
|
||||
: unarchiveChore.mutate(choreId)
|
||||
}}
|
||||
>
|
||||
{isActive ? 'Archive' : 'Unarchive'}
|
||||
{isActive ? t('choreEdit.archive') : t('choreEdit.unarchive')}
|
||||
</Button>
|
||||
<MenuButton
|
||||
slots={{ root: IconButton }}
|
||||
@@ -1987,7 +2002,7 @@ const ChoreEdit = () => {
|
||||
</ButtonGroup>
|
||||
<Menu placement='top-end'>
|
||||
<MenuItem color='danger' onClick={handleDelete}>
|
||||
Delete
|
||||
{t('common:delete')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
@@ -1999,13 +2014,13 @@ const ChoreEdit = () => {
|
||||
window.history.back()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
{showKeyboardShortcuts && (
|
||||
<KeyboardShortcutHint shortcut='Esc' sx={{ ml: 1 }} />
|
||||
)}
|
||||
</Button>
|
||||
<Button color='primary' variant='solid' onClick={HandleSaveChore}>
|
||||
{choreId > 0 ? 'Save' : 'Create'}
|
||||
{choreId > 0 ? t('common:save') : t('choreEdit.create')}
|
||||
{showKeyboardShortcuts && (
|
||||
<KeyboardShortcutHint shortcut='Enter' sx={{ ml: 1 }} />
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ import { Purchases } from '@revenuecat/purchases-capacitor'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import RealTimeSettings from '../../components/RealTimeSettings'
|
||||
@@ -57,6 +58,7 @@ import StorageSettings from './StorageSettings'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
const Settings = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const queryClient = useQueryClient()
|
||||
const { showNotification } = useNotification()
|
||||
@@ -83,8 +85,8 @@ const Settings = () => {
|
||||
message,
|
||||
title,
|
||||
onConfirm,
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
confirmText = t('common.confirm'),
|
||||
cancelText = t('common.cancel'),
|
||||
color = 'primary',
|
||||
) => {
|
||||
setConfirmModalConfig({
|
||||
@@ -112,7 +114,7 @@ const Settings = () => {
|
||||
} catch (error) {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to refresh member requests',
|
||||
message: t('circleSettings.refreshFailed'),
|
||||
})
|
||||
} finally {
|
||||
setIsRefreshing(false)
|
||||
@@ -195,27 +197,29 @@ const Settings = () => {
|
||||
|
||||
const getSubscriptionDetails = () => {
|
||||
if (userProfile?.subscription === 'active') {
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(
|
||||
userProfile?.expiration,
|
||||
)}.`
|
||||
return t('accountSettings.activeDescription', {
|
||||
date: fmt.date(userProfile?.expiration),
|
||||
})
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(
|
||||
userProfile?.expiration,
|
||||
)}.`
|
||||
return t('accountSettings.cancelledDescription', {
|
||||
date: fmt.date(userProfile?.expiration),
|
||||
})
|
||||
} else {
|
||||
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||
return t('accountSettings.freeDescription')
|
||||
}
|
||||
}
|
||||
const getSubscriptionStatus = () => {
|
||||
if (userProfile?.subscription === 'active') {
|
||||
return `Plus`
|
||||
return t('accountSettings.plus')
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
if (moment().isBefore(userProfile?.expiration)) {
|
||||
return `Plus(until ${fmt.date(userProfile?.expiration)})`
|
||||
return t('accountSettings.plusUntil', {
|
||||
date: fmt.date(userProfile?.expiration),
|
||||
})
|
||||
}
|
||||
return `Free`
|
||||
return t('accountSettings.free')
|
||||
} else {
|
||||
return `Free`
|
||||
return t('accountSettings.free')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,18 +245,15 @@ const Settings = () => {
|
||||
<Container>
|
||||
<ProfileSettings />
|
||||
<div className='grid gap-4 py-4' id='circle'>
|
||||
<Typography level='h3'>Circle settings</Typography>
|
||||
<Typography level='h3'>{t('circleSettings.title')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Your account is automatically connected to a Circle when you create or
|
||||
join one. Easily invite friends by sharing the unique Circle code or
|
||||
link below. You'll receive a notification below when someone requests
|
||||
to join your Circle.
|
||||
{t('circleSettings.description')}
|
||||
</Typography>
|
||||
<Typography level='title-sm' mb={-1}>
|
||||
{userCircles[0]?.userRole === 'member'
|
||||
? `You part of ${userCircles[0]?.name} `
|
||||
: `You circle code is:`}
|
||||
? t('circleSettings.memberOf', { name: userCircles[0]?.name })
|
||||
: t('circleSettings.yourCircleCode')}
|
||||
|
||||
<Input
|
||||
value={userCircles[0]?.invite_code}
|
||||
@@ -269,11 +270,11 @@ const Settings = () => {
|
||||
navigator.clipboard.writeText(userCircles[0]?.invite_code)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Code copied to clipboard',
|
||||
message: t('circleSettings.codeCopied'),
|
||||
})
|
||||
}}
|
||||
>
|
||||
Copy Code
|
||||
{t('circleSettings.copyCode')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='soft'
|
||||
@@ -287,11 +288,11 @@ const Settings = () => {
|
||||
)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Link copied to clipboard',
|
||||
message: t('circleSettings.linkCopied'),
|
||||
})
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
{t('circleSettings.copyLink')}
|
||||
</Button>
|
||||
{userCircles.length > 0 && userCircles[0]?.userRole === 'member' && (
|
||||
<Button
|
||||
@@ -300,35 +301,37 @@ const Settings = () => {
|
||||
sx={{ ml: 1 }}
|
||||
onClick={() => {
|
||||
showConfirmation(
|
||||
'Are you sure you want to leave your circle?',
|
||||
'Leave Circle',
|
||||
t('circleSettings.leaveConfirmMessage'),
|
||||
t('circleSettings.leaveConfirmTitle'),
|
||||
() => {
|
||||
LeaveCircle(userCircles[0]?.id).then(resp => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Left circle successfully',
|
||||
message: t('circleSettings.leftCircle'),
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to leave circle',
|
||||
message: t('circleSettings.leaveFailed'),
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
'Leave',
|
||||
'Cancel',
|
||||
t('circleSettings.leaveConfirmButton'),
|
||||
t('common.cancel'),
|
||||
'danger',
|
||||
)
|
||||
}}
|
||||
>
|
||||
Leave Circle
|
||||
{t('circleSettings.leave')}
|
||||
</Button>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Typography level='title-md'>Circle Members</Typography>
|
||||
<Typography level='title-md'>
|
||||
{t('circleSettings.circleMembers')}
|
||||
</Typography>
|
||||
{circleMembers.map(member => (
|
||||
<Card key={member.id} className='p-4'>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
@@ -336,19 +339,27 @@ const Settings = () => {
|
||||
<Typography level='body-md'>
|
||||
{member.displayName.charAt(0).toUpperCase() +
|
||||
member.displayName.slice(1)}
|
||||
{member.userId === userProfile.id ? '(You)' : ''}{' '}
|
||||
{member.userId === userProfile.id
|
||||
? t('circleSettings.you')
|
||||
: ''}{' '}
|
||||
<Chip>
|
||||
{' '}
|
||||
{member.isActive ? member.role : 'Pending Approval'}
|
||||
{member.isActive
|
||||
? member.role
|
||||
: t('circleSettings.pendingApproval')}
|
||||
</Chip>
|
||||
</Typography>
|
||||
{member.isActive ? (
|
||||
<Typography level='body-sm'>
|
||||
Joined on {fmt.date(member.createdAt)}
|
||||
{t('circleSettings.joinedOn', {
|
||||
date: fmt.date(member.createdAt),
|
||||
})}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography level='body-sm' color='danger'>
|
||||
Request to join {fmt.date(member.updatedAt)}
|
||||
{t('circleSettings.requestedToJoin', {
|
||||
date: fmt.date(member.updatedAt),
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -378,28 +389,14 @@ const Settings = () => {
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to update role',
|
||||
message: t('circleSettings.roleUpdateFailed'),
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'member',
|
||||
description: 'Just a regular member of the circle',
|
||||
},
|
||||
{
|
||||
value: 'manager',
|
||||
description:
|
||||
'Can impersonate users and perform actions on their behalf',
|
||||
},
|
||||
{
|
||||
value: 'admin',
|
||||
description: 'Full access to the circle',
|
||||
},
|
||||
].map((option, index) => (
|
||||
<Option value={option.value} key={index}>
|
||||
{['member', 'manager', 'admin'].map((option, index) => (
|
||||
<Option value={option} key={index}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -414,14 +411,13 @@ const Settings = () => {
|
||||
level='title-sm'
|
||||
sx={{ mb: 0, mt: 0, lineHeight: 1.1 }}
|
||||
>
|
||||
{option.value.charAt(0).toUpperCase() +
|
||||
option.value.slice(1)}
|
||||
{t(`circleSettings.roles.${option}`)}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ mt: 0, mb: 0, lineHeight: 1.1 }}
|
||||
>
|
||||
{option.description}
|
||||
{t(`circleSettings.roles.${option}Description`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
@@ -437,8 +433,10 @@ const Settings = () => {
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
showConfirmation(
|
||||
`Are you sure you want to remove ${member.displayName} from your circle?`,
|
||||
'Remove Member',
|
||||
t('circleSettings.removeMemberMessage', {
|
||||
name: member.displayName,
|
||||
}),
|
||||
t('circleSettings.removeMemberTitle'),
|
||||
() => {
|
||||
DeleteCircleMember(
|
||||
member.circleId,
|
||||
@@ -447,7 +445,7 @@ const Settings = () => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Removed member successfully',
|
||||
message: t('circleSettings.memberRemoved'),
|
||||
})
|
||||
// Invalidate and refetch circle-related queries
|
||||
queryClient.invalidateQueries(['circleMembers'])
|
||||
@@ -463,8 +461,8 @@ const Settings = () => {
|
||||
}
|
||||
})
|
||||
},
|
||||
'Remove',
|
||||
'Cancel',
|
||||
t('common.remove'),
|
||||
t('common.cancel'),
|
||||
'danger',
|
||||
)
|
||||
}}
|
||||
@@ -485,11 +483,15 @@ const Settings = () => {
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography level='title-md'>Circle Member Requests</Typography>
|
||||
<Typography level='title-md'>
|
||||
{t('circleSettings.circleMemberRequests')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{lastRefresh && (
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Last updated: {fmt.dateTime(lastRefresh)}
|
||||
{t('circleSettings.lastUpdated', {
|
||||
time: fmt.dateTime(lastRefresh),
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
@@ -501,7 +503,9 @@ const Settings = () => {
|
||||
isRefreshing ? <CircularProgress size='sm' /> : <Refresh />
|
||||
}
|
||||
>
|
||||
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||
{isRefreshing
|
||||
? t('circleSettings.refreshing')
|
||||
: t('common.refresh')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -509,21 +513,24 @@ const Settings = () => {
|
||||
{circleMemberRequests.map(request => (
|
||||
<Card key={request.id} className='p-4'>
|
||||
<Typography level='body-md'>
|
||||
{request.displayName} wants to join your circle.
|
||||
{t('circleSettings.wantsToJoin', { name: request.displayName })}
|
||||
</Typography>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
showConfirmation(
|
||||
`Are you sure you want to accept ${request.displayName} (username: ${request.username}) to join your circle?`,
|
||||
'Accept Member Request',
|
||||
t('circleSettings.acceptRequestMessage', {
|
||||
name: request.displayName,
|
||||
username: request.username,
|
||||
}),
|
||||
t('circleSettings.acceptRequestTitle'),
|
||||
() => {
|
||||
AcceptCircleMemberRequest(request.id).then(resp => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Accepted request successfully',
|
||||
message: t('circleSettings.requestAccepted'),
|
||||
})
|
||||
// Invalidate and refetch circle-related queries
|
||||
queryClient.invalidateQueries(['circleMembers'])
|
||||
@@ -540,26 +547,25 @@ const Settings = () => {
|
||||
}
|
||||
})
|
||||
},
|
||||
'Accept',
|
||||
'Cancel',
|
||||
t('circleSettings.accept'),
|
||||
t('common.cancel'),
|
||||
)
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
{t('circleSettings.accept')}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
<Divider> or </Divider>
|
||||
<Divider> {t('circleSettings.or')} </Divider>
|
||||
|
||||
<Typography level='body-md'>
|
||||
if want to join someone else's Circle? Ask them for their unique
|
||||
Circle code or join link. Enter the code below to join their Circle.
|
||||
{t('circleSettings.joinOtherDescription')}
|
||||
</Typography>
|
||||
|
||||
<Typography level='title-sm' mb={-1}>
|
||||
Enter Circle code:
|
||||
{t('circleSettings.enterCircleCode')}
|
||||
<Input
|
||||
placeholder='Enter code'
|
||||
placeholder={t('circleSettings.enterCodePlaceholder')}
|
||||
value={circleInviteCode}
|
||||
onChange={e => setCircleInviteCode(e.target.value)}
|
||||
size='lg'
|
||||
@@ -575,20 +581,19 @@ const Settings = () => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message:
|
||||
'Joined circle successfully, wait for the circle owner to accept your request.',
|
||||
message: t('circleSettings.joinedPending'),
|
||||
})
|
||||
setTimeout(() => navigate('/'), 3000)
|
||||
} else {
|
||||
if (resp.status === 409) {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'You are already a member of this circle',
|
||||
message: t('circleSettings.alreadyMember'),
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to join circle',
|
||||
message: t('circleSettings.joinFailed'),
|
||||
})
|
||||
}
|
||||
setTimeout(() => navigate('/'), 3000)
|
||||
@@ -596,24 +601,21 @@ const Settings = () => {
|
||||
})
|
||||
}}
|
||||
>
|
||||
Join Circle
|
||||
{t('circleSettings.joinCircle')}
|
||||
</Button>
|
||||
</Typography>
|
||||
{circleMembers.find(m => userProfile.id == m.userId)?.role ===
|
||||
'admin' && (
|
||||
<>
|
||||
<Typography level='title-lg' mt={2}>
|
||||
Webhook
|
||||
{t('advanced.webhookTitle')}
|
||||
</Typography>
|
||||
<Typography level='body-md' mt={-1}>
|
||||
Webhooks allow you to send real-time notifications to other
|
||||
services when events happen in your Circle. Configure a webhook
|
||||
URL to receive real-time updates.
|
||||
{t('advanced.webhookDescription')}
|
||||
</Typography>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Webhook notifications are not available in the Basic plan.
|
||||
Upgrade to Plus to receive real-time updates via webhooks.
|
||||
{t('advanced.webhookPlusNotice')}
|
||||
</Typography>
|
||||
)}
|
||||
<FormControl sx={{ mt: 1 }}>
|
||||
@@ -627,7 +629,7 @@ const Settings = () => {
|
||||
}
|
||||
}}
|
||||
variant='soft'
|
||||
label='Enable Webhook'
|
||||
label={t('advanced.webhookToggle')}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
overlay
|
||||
/>
|
||||
@@ -636,10 +638,10 @@ const Settings = () => {
|
||||
opacity: !isPlusAccount(userProfile) ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
Enable webhook notifications for tasks and things updates.{' '}
|
||||
{t('advanced.webhookHelper')}{' '}
|
||||
{userProfile && !isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning'>
|
||||
Plus Feature
|
||||
{t('common.plusFeature')}
|
||||
</Chip>
|
||||
)}
|
||||
</FormHelperText>
|
||||
@@ -647,7 +649,9 @@ const Settings = () => {
|
||||
|
||||
{webhookURL !== null && (
|
||||
<Box>
|
||||
<Typography level='title-sm'>Webhook URL</Typography>
|
||||
<Typography level='title-sm'>
|
||||
{t('advanced.webhookURL')}
|
||||
</Typography>
|
||||
<Input
|
||||
value={webhookURL ? webhookURL : ''}
|
||||
onChange={e => setWebhookURL(e.target.value)}
|
||||
@@ -670,19 +674,19 @@ const Settings = () => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Webhook URL updated successfully',
|
||||
message: t('advanced.webhookUpdated'),
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to update webhook URL',
|
||||
message: t('advanced.webhookUpdateFailed'),
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
>
|
||||
Save
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -695,13 +699,13 @@ const Settings = () => {
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 py-4' id='account'>
|
||||
<Typography level='h3'>Account Settings</Typography>
|
||||
<Typography level='h3'>{t('accountSettings.title')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Change your account settings, type or update your password
|
||||
{t('accountSettings.description')}
|
||||
</Typography>
|
||||
<Typography level='title-md' mb={-1}>
|
||||
Account Type : {getSubscriptionStatus()}
|
||||
{t('accountSettings.accountType', { type: getSubscriptionStatus() })}
|
||||
</Typography>
|
||||
<Typography level='body-sm'>{getSubscriptionDetails()}</Typography>
|
||||
<Box>
|
||||
@@ -733,8 +737,7 @@ const Settings = () => {
|
||||
queryClient.refetchQueries(['userProfile'])
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message:
|
||||
'Purchase successful! Please restart the app to access Plus features.',
|
||||
message: t('accountSettings.purchase.success'),
|
||||
})
|
||||
// invalidate user profile to get new subscription status:
|
||||
}
|
||||
@@ -749,57 +752,49 @@ const Settings = () => {
|
||||
// Store problem
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Store connection issue. Please check your network and try again.',
|
||||
message: t('accountSettings.purchase.storeConnection'),
|
||||
})
|
||||
} else if (error.code === '3') {
|
||||
// Purchase not allowed
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Purchases are not allowed on this device. Please check your device restrictions.',
|
||||
message: t('accountSettings.purchase.notAllowed'),
|
||||
})
|
||||
} else if (error.code === '4') {
|
||||
// Product not available
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'This subscription is not available. Please try again later.',
|
||||
message: t('accountSettings.purchase.unavailable'),
|
||||
})
|
||||
} else if (error.code === '5') {
|
||||
// Receipt already in use
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'This purchase has already been processed. If you believe this is an error, please contact support.',
|
||||
message: t('accountSettings.purchase.alreadyProcessed'),
|
||||
})
|
||||
} else if (error.code === '6') {
|
||||
// Missing receipt file
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Purchase receipt missing. Please try purchasing again.',
|
||||
message: t('accountSettings.purchase.receiptMissing'),
|
||||
})
|
||||
} else if (error.code === '7') {
|
||||
// Network error
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Network error. Please check your connection and try again.',
|
||||
message: t('accountSettings.purchase.networkError'),
|
||||
})
|
||||
} else if (error.code === '8') {
|
||||
// Invalid receipt
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Invalid purchase receipt. Please contact support if this persists.',
|
||||
message: t('accountSettings.purchase.invalidReceipt'),
|
||||
})
|
||||
} else if (error.code === '9') {
|
||||
// Payment pending
|
||||
showNotification({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Payment is pending approval. You will receive access once approved.',
|
||||
message: t('accountSettings.purchase.pending'),
|
||||
})
|
||||
} else {
|
||||
// Generic error
|
||||
@@ -808,7 +803,11 @@ const Settings = () => {
|
||||
console.error('Error occurred in purchase flow')
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: `Purchase failed: ${error.message || 'Unknown error'}. Please try again or contact support.`,
|
||||
message: t('accountSettings.purchase.failed', {
|
||||
error:
|
||||
error.message ||
|
||||
t('accountSettings.purchase.unknownError'),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -817,7 +816,7 @@ const Settings = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
{t('accountSettings.upgrade')}
|
||||
</Button>
|
||||
|
||||
{userProfile?.subscription === 'active' && (
|
||||
@@ -833,14 +832,14 @@ const Settings = () => {
|
||||
setNativeCancelModal(true)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('accountSettings.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{import.meta.env.VITE_IS_SELF_HOSTED === 'true' && (
|
||||
<Box>
|
||||
<Typography level='title-md' mb={1}>
|
||||
Password :
|
||||
{t('accountSettings.password')}
|
||||
</Typography>
|
||||
<Typography mb={1} level='body-sm'></Typography>
|
||||
<Button
|
||||
@@ -849,7 +848,7 @@ const Settings = () => {
|
||||
setChangePasswordModal(true)
|
||||
}}
|
||||
>
|
||||
Change Password
|
||||
{t('accountSettings.changePassword')}
|
||||
</Button>
|
||||
{changePasswordModal ? (
|
||||
<PassowrdChangeModal
|
||||
@@ -860,12 +859,12 @@ const Settings = () => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Password changed successfully',
|
||||
message: t('accountSettings.passwordChanged'),
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Password change failed',
|
||||
message: t('accountSettings.passwordChangeFailed'),
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -879,18 +878,17 @@ const Settings = () => {
|
||||
|
||||
<Box>
|
||||
<Typography level='title-md' mb={1} color='danger'>
|
||||
Danger Zone
|
||||
{t('accountSettings.dangerZone')}
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2} color='neutral'>
|
||||
Once you delete your account, there is no going back. Please be
|
||||
certain.
|
||||
{t('accountSettings.dangerZoneDescription')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() => setUserDeletionModal(true)}
|
||||
>
|
||||
Delete Account
|
||||
{t('accountSettings.deleteAccount')}
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
@@ -899,32 +897,26 @@ const Settings = () => {
|
||||
<APITokenSettings />
|
||||
<StorageSettings />
|
||||
<div className='grid gap-4 py-4' id='sidepanel'>
|
||||
<Typography level='h3'>Sidepanel Customization</Typography>
|
||||
<Typography level='h3'>{t('sidepanel.title')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Customize the layout and visibility of cards in the sidepanel. the
|
||||
section only available on large screen devices such as tablets and
|
||||
desktops..
|
||||
{t('sidepanel.detailedDescription')}
|
||||
</Typography>
|
||||
<SidepanelSettings />
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 py-4' id='theme'>
|
||||
<Typography level='h3'>Theme preferences</Typography>
|
||||
<Typography level='h3'>{t('theme.title')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Choose how the site looks to you. Select a single theme, or sync with
|
||||
your system and automatically switch between day and night themes.
|
||||
</Typography>
|
||||
<Typography level='body-md'>{t('theme.description')}</Typography>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 py-4' id='localization'>
|
||||
<Typography level='h3'>Localization</Typography>
|
||||
<Typography level='h3'>{t('localization.title')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Customize language, date format, and regional preferences for your
|
||||
account. These settings will apply throughout the application.
|
||||
{t('localization.descriptionLong')}
|
||||
</Typography>
|
||||
<LocalizationSettings />
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as chrono from 'chrono-node'
|
||||
import moment from 'moment'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import ModalActions from '../../components/common/ModalActions'
|
||||
@@ -186,6 +187,7 @@ const POINTS_SUGGESTIONS = {
|
||||
const PARSE_DEBOUNCE_MS = 150
|
||||
|
||||
const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
const { t } = useTranslation('chores')
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
|
||||
const pickerEmptyDisplay = isMobile ? 'icon' : 'icon-text'
|
||||
@@ -229,13 +231,13 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
value: 'userId',
|
||||
display: 'displayName',
|
||||
options: [
|
||||
{ userId: 'anyone', displayName: 'Anyone' },
|
||||
{ userId: 'anyone', displayName: t('addTask.anyone') },
|
||||
...(circleMembers?.res || []),
|
||||
],
|
||||
},
|
||||
'*': POINTS_SUGGESTIONS,
|
||||
}),
|
||||
[userLabels, circleMembers, handleCreateLabel],
|
||||
[userLabels, circleMembers, handleCreateLabel, t],
|
||||
)
|
||||
|
||||
const [taskText, setTaskText] = useState('')
|
||||
@@ -1156,7 +1158,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
onClose={handleCloseModal}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
title='Create new task'
|
||||
title={t('addTask.title')}
|
||||
footer={
|
||||
<ModalActions>
|
||||
{!showScan && !showVoice && projects?.length >= 1 && (
|
||||
@@ -1177,7 +1179,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
>
|
||||
{selectedProject.name}
|
||||
{selectedProject.id === 'default'
|
||||
? t('addTask.defaultProject')
|
||||
: selectedProject.name}
|
||||
</MenuButton>
|
||||
<Menu
|
||||
placement='top-start'
|
||||
@@ -1199,7 +1203,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
sx={{ fontSize: 18, color: project.color }}
|
||||
/>
|
||||
</ListItemDecorator>
|
||||
{project.name}
|
||||
{project.id === 'default'
|
||||
? t('addTask.defaultProject')
|
||||
: project.name}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
@@ -1211,7 +1217,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
color='neutral'
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
{showKeyboardShortcuts && (
|
||||
<KeyboardShortcutHint
|
||||
shortcut='Esc'
|
||||
@@ -1231,10 +1237,12 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
onClick={handleVoiceConfirm}
|
||||
>
|
||||
{creatingVoiceTasks
|
||||
? 'Creating…'
|
||||
? t('addTask.creating')
|
||||
: voiceState.segments.length > 1
|
||||
? `Create ${voiceState.segments.length} Tasks`
|
||||
: 'Use Task'}
|
||||
? t('addTask.createCount', {
|
||||
count: voiceState.segments.length,
|
||||
})
|
||||
: t('addTask.useTask')}
|
||||
</Button>
|
||||
)}
|
||||
{showScan && scanState.primaryAction && (
|
||||
@@ -1249,7 +1257,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
)}
|
||||
{showScan && scanState.phase === 'processing' && (
|
||||
<Button variant='solid' color='primary' loading disabled>
|
||||
Processing
|
||||
{t('addTask.processing')}
|
||||
</Button>
|
||||
)}
|
||||
{!showScan && !showVoice && (
|
||||
@@ -1260,7 +1268,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
disabled={!taskTitle.trim() || isAttachingScan}
|
||||
onClick={submitChore}
|
||||
>
|
||||
Create
|
||||
{t('addTask.create')}
|
||||
{showKeyboardShortcuts && (
|
||||
<KeyboardShortcutHint shortcut='Enter' sx={{ ml: 1 }} />
|
||||
)}
|
||||
@@ -1349,7 +1357,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
onVoiceClick={
|
||||
voiceAvailable ? () => setShowVoice(true) : undefined
|
||||
}
|
||||
placeholder='Type your task...'
|
||||
placeholder={t('addTask.taskPlaceholder')}
|
||||
onChange={text => {
|
||||
setTaskText(text)
|
||||
if (!text) setTaskTitle('')
|
||||
@@ -1484,7 +1492,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
}}
|
||||
>
|
||||
<Add sx={{ fontSize: 20 }} />
|
||||
<Typography level='body-sm'>Description</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{t('addTask.descriptionChip')}
|
||||
</Typography>
|
||||
</Button>
|
||||
)}
|
||||
{!hasSubTasks && (
|
||||
@@ -1507,7 +1517,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
|
||||
}}
|
||||
>
|
||||
<Add sx={{ fontSize: 20 }} />
|
||||
<Typography level='body-sm'>Subtasks</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{t('addTask.subtasksChip')}
|
||||
</Typography>
|
||||
</Button>
|
||||
)}
|
||||
<AdvancedOptionsTrigger
|
||||
|
||||
Reference in New Issue
Block a user