Files
donetick/src/views/Modals/Inputs/NudgeModal.jsx
everysingletear 7d36090e46 i18n: extract the chore list and its modals (chores namespace)
Part of #145.

Twenty-three files across the task list zone: the list and card views,
sorting and grouping, multi-select and its toolbar and help sheet,
archived tasks, the assignee card, the chore action menu, the
nudge/NFC/photo modals, the rich text editor, the scan panel, the
notification templates and the keyboard-shortcut toasts.

Extends the existing `chores` namespace, so `src/i18n/config.js` is
untouched.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (247 call sites).

Two values are matched loosely and worth naming: archived.closeMultiSelect
in both the archived view and the toolbar. The base builds that tooltip as
`${size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`, so only one branch
of the ternary exists contiguously in the source. Both keys hold exactly
what each branch renders; the tooltip is kept whole so a translator can
reorder it.

Rebased on current `develop` again after #215 and #216 landed — the
dictionary conflict was theirs, not the code's. No code file in this PR
was touched upstream in the meantime.
2026-08-15 09:03:28 +08:00

176 lines
5.1 KiB
JavaScript

import {
Alert,
Box,
FormControl,
FormLabel,
Switch,
Textarea,
Typography,
} from '@mui/joy'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { isOfficialDonetickInstanceSync } from '../../../utils/FeatureToggle'
function NudgeModal({ config }) {
const { t } = useTranslation('chores')
const { ResponsiveModal } = useResponsiveModal()
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [message, setMessage] = useState('')
const [notifyAllAssignees, setNotifyAllAssignees] = useState(false)
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
const handleAction = useCallback(
isConfirmed => {
if (isConfirmed) {
config.onConfirm({
choreId: config.choreId,
message,
notifyAllAssignees,
})
} else {
config.onClose()
}
},
[config, message, notifyAllAssignees],
)
// Reset form when modal opens
useEffect(() => {
if (config?.isOpen) {
setMessage('')
setNotifyAllAssignees(false)
// Check if this is the official donetick.com instance
try {
setIsOfficialInstance(isOfficialDonetickInstanceSync())
} catch (error) {
console.warn('Error checking instance type:', error)
setIsOfficialInstance(false)
}
}
}, [config?.isOpen])
// Keyboard shortcuts for nudge modal
useEffect(() => {
const handleKeyDown = event => {
if (!config?.isOpen) return
// Show keyboard shortcuts when Ctrl/Cmd is pressed
if (event.ctrlKey || event.metaKey) {
setShowKeyboardShortcuts(true)
}
// Ctrl/Cmd + Y for confirm
if ((event.ctrlKey || event.metaKey) && event.key === 'y') {
event.preventDefault()
handleAction(true)
return
}
// Ctrl/Cmd + X for cancel
if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
event.preventDefault()
handleAction(false)
return
}
// Escape key for cancel
if (event.key === 'Escape') {
event.preventDefault()
handleAction(false)
return
}
}
const handleKeyUp = event => {
if (!event.ctrlKey && !event.metaKey) {
setShowKeyboardShortcuts(false)
}
}
if (config?.isOpen) {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [config?.isOpen, handleAction])
return (
<ResponsiveModal
open={config?.isOpen}
onClose={config?.onClose}
size='lg'
fullWidth={true}
unmountDelay={250}
title={t('nudge.title')}
description='Send a gentle reminder to the people assigned to this task.'
footer={
<ModalActions
secondary={{
label: t('choreView.cancel'),
onClick: () => handleAction(false),
endDecorator: showKeyboardShortcuts ? (
<KeyboardShortcutHint shortcut='X' />
) : undefined,
}}
primary={{
label: t('nudge.title'),
onClick: () => handleAction(true),
disabled: !isOfficialInstance,
endDecorator: showKeyboardShortcuts ? (
<KeyboardShortcutHint shortcut='Y' />
) : undefined,
}}
/>
}
>
{!isOfficialInstance && (
<Alert color='warning' sx={{ mb: 2 }}>
<Typography level='body-sm'>
<strong>Heads up!</strong>This feature avaiable on Donetick Cloud!
Since you&apos;re using a self-hosted instance, nudges will requires
you to setup Google cloud account and Firebase Cloud Messaging
(FCM). and build the Android or the iOS app by yourself.
<br />
Will update if we come up with a solution to make this easier for to
configure. for selfhosters
</Typography>
</Alert>
)}
<FormControl mb={2}>
<FormLabel>{t('nudge.customMessage')}</FormLabel>
<Textarea
placeholder={t('nudge.messagePlaceholder')}
value={message}
onChange={e => setMessage(e.target.value)}
minRows={3}
maxRows={5}
/>
</FormControl>
<FormControl orientation='horizontal' sx={{ mb: 3 }}>
<Box sx={{ flex: 1 }}>
<FormLabel>{t('nudge.notifyAll')}</FormLabel>
<Typography level='body-sm' color='text.secondary'>
{t('nudge.notifyAllHint')}
</Typography>
</Box>
<Switch
checked={notifyAllAssignees}
onChange={e => setNotifyAllAssignees(e.target.checked)}
/>
</FormControl>
</ResponsiveModal>
)
}
export default NudgeModal