Unify modals and buttons style and layout
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { Box, Button, FormLabel, Input } from '@mui/joy'
|
||||
import { FormLabel, Input } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import ModalActions from '../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
import ConfirmationModal from './Inputs/ConfirmationModal'
|
||||
|
||||
@@ -41,31 +42,19 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
// fullWidth={true}
|
||||
title='Edit History'
|
||||
footer={
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() =>
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: config.onClose }}
|
||||
primary={{
|
||||
label: 'Save',
|
||||
onClick: () =>
|
||||
config.onSave({
|
||||
id: historyRecord.id,
|
||||
performedAt: moment(completedDate).toISOString(),
|
||||
dueDate: moment(dueDate).toISOString(),
|
||||
notes,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={config.onClose}
|
||||
variant='outlined'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormLabel>Due Date</FormLabel>
|
||||
@@ -119,6 +108,7 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
message: 'Are you sure you want to delete this history?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
color: 'danger',
|
||||
}}
|
||||
/>
|
||||
</ResponsiveModal>
|
||||
|
||||
@@ -15,28 +15,40 @@ import {
|
||||
import { Avatar, Box, Button, Chip, Divider, Stack, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import ModalActions from '../../components/common/ModalActions'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
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 /> },
|
||||
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 /> },
|
||||
4: { label: 'Rejected', color: 'danger', icon: <ThumbDown /> },
|
||||
5: { label: 'Missed', color: 'danger', icon: <RunningWithErrors /> },
|
||||
6: { label: 'Rescheduled', color: 'warning', icon: <Schedule /> },
|
||||
}
|
||||
|
||||
const DetailRow = ({ icon, label, value, children }) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5, py: 0.75 }}>
|
||||
<Box sx={{ color: 'text.tertiary', mt: 0.25, flexShrink: 0, display: 'flex' }}>{icon}</Box>
|
||||
<Box
|
||||
sx={{ color: 'text.tertiary', mt: 0.25, flexShrink: 0, display: 'flex' }}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary', mb: 0.15 }}>{label}</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary', mb: 0.15 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{children ?? (
|
||||
<Typography level='body-sm' sx={{ color: 'text.primary', fontWeight: 'md' }}>{value}</Typography>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'text.primary', fontWeight: 'md' }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -52,15 +64,41 @@ const TimingBadge = ({ historyEntry }) => {
|
||||
const gracePeriod = 6 * 60 * 60 * 1000
|
||||
|
||||
if (Math.abs(performedAt - dueDate) <= gracePeriod) {
|
||||
return <Chip size='sm' variant='solid' sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }} startDecorator={<Check />}>On Time</Chip>
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='solid'
|
||||
sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }}
|
||||
startDecorator={<Check />}
|
||||
>
|
||||
On Time
|
||||
</Chip>
|
||||
)
|
||||
} else if (performedAt.isBefore(dueDate)) {
|
||||
const abs = Math.abs(diffHours)
|
||||
const label = abs >= 48 ? `${Math.floor(abs / 24)}d early` : `${abs}h early`
|
||||
return <Chip size='sm' variant='soft' sx={{ backgroundColor: TASK_COLOR.SCHEDULED, color: 'white' }} startDecorator={<Check />}>{label}</Chip>
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
sx={{ backgroundColor: TASK_COLOR.SCHEDULED, color: 'white' }}
|
||||
startDecorator={<Check />}
|
||||
>
|
||||
{label}
|
||||
</Chip>
|
||||
)
|
||||
} else {
|
||||
const abs = Math.abs(diffHours)
|
||||
const label = abs >= 48 ? `${Math.floor(abs / 24)}d late` : `${abs}h late`
|
||||
return <Chip size='sm' variant='solid' sx={{ backgroundColor: TASK_COLOR.LATE, color: 'white' }}>{label}</Chip>
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='solid'
|
||||
sx={{ backgroundColor: TASK_COLOR.LATE, color: 'white' }}
|
||||
>
|
||||
{label}
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +117,8 @@ function HistoryDetailModal({ config }) {
|
||||
const statusLabel = isFirstSchedule ? 'Scheduled' : statusCfg.label
|
||||
const performer = performers.find(p => p.userId === entry.completedBy)
|
||||
const assignedTo = performers.find(p => p.userId === entry.assignedTo)
|
||||
const isDifferentAssignee = entry.assignedTo && entry.completedBy !== entry.assignedTo
|
||||
const isDifferentAssignee =
|
||||
entry.assignedTo && entry.completedBy !== entry.assignedTo
|
||||
|
||||
// updatedAt is only meaningful if it differs from performedAt by more than a minute
|
||||
const showUpdatedAt =
|
||||
@@ -100,14 +139,50 @@ function HistoryDetailModal({ config }) {
|
||||
open={config?.isOpen}
|
||||
onClose={config?.onClose}
|
||||
title='Activity Detail'
|
||||
footer={
|
||||
<ModalActions>
|
||||
{entry.choreId && (
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<OpenInNew sx={{ fontSize: 16 }} />}
|
||||
onClick={() => {
|
||||
config?.onClose?.()
|
||||
navigate(`/chores/${entry.choreId}`)
|
||||
}}
|
||||
>
|
||||
Open Task
|
||||
</Button>
|
||||
)}
|
||||
{config?.onEdit && (
|
||||
<Button
|
||||
startDecorator={<Edit sx={{ fontSize: 16 }} />}
|
||||
onClick={() => config.onEdit(entry)}
|
||||
>
|
||||
Edit Entry
|
||||
</Button>
|
||||
)}
|
||||
</ModalActions>
|
||||
}
|
||||
>
|
||||
{/* Status header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Avatar size='sm' color={statusCfg.color} variant='soft'>
|
||||
{statusCfg.icon}
|
||||
</Avatar>
|
||||
<Typography level='title-md' fontWeight='lg' sx={{ color: `${statusCfg.color}.plainColor` }}>
|
||||
<Typography
|
||||
level='title-md'
|
||||
fontWeight='lg'
|
||||
sx={{ color: `${statusCfg.color}.plainColor` }}
|
||||
>
|
||||
{statusLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -119,17 +194,31 @@ function HistoryDetailModal({ config }) {
|
||||
<Stack spacing={0}>
|
||||
{/* Who performed it */}
|
||||
{performer && (
|
||||
<DetailRow icon={<Check sx={{ fontSize: 16 }} />} label='Performed by'>
|
||||
<DetailRow
|
||||
icon={<Check sx={{ fontSize: 16 }} />}
|
||||
label='Performed by'
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Avatar src={performer.image} alt={performer.displayName} size='sm' sx={{ width: 20, height: 20 }} />
|
||||
<Typography level='body-sm' fontWeight='md'>{performer.displayName}</Typography>
|
||||
<Avatar
|
||||
src={performer.image}
|
||||
alt={performer.displayName}
|
||||
size='sm'
|
||||
sx={{ width: 20, height: 20 }}
|
||||
/>
|
||||
<Typography level='body-sm' fontWeight='md'>
|
||||
{performer.displayName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DetailRow>
|
||||
)}
|
||||
|
||||
{/* Assigned to (only if different) */}
|
||||
{isDifferentAssignee && assignedTo && (
|
||||
<DetailRow icon={<Person sx={{ fontSize: 16 }} />} label='Assigned to' value={assignedTo.displayName} />
|
||||
<DetailRow
|
||||
icon={<Person sx={{ fontSize: 16 }} />}
|
||||
label='Assigned to'
|
||||
value={assignedTo.displayName}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
@@ -138,7 +227,15 @@ function HistoryDetailModal({ config }) {
|
||||
{entry.performedAt && (
|
||||
<DetailRow
|
||||
icon={<AccessTime sx={{ fontSize: 16 }} />}
|
||||
label={isFirstSchedule ? 'Scheduled on' : entry.status === 6 ? 'Rescheduled on' : entry.status === 2 ? 'Skipped on' : 'Completed on'}
|
||||
label={
|
||||
isFirstSchedule
|
||||
? 'Scheduled on'
|
||||
: entry.status === 6
|
||||
? 'Rescheduled on'
|
||||
: entry.status === 2
|
||||
? 'Skipped on'
|
||||
: 'Completed on'
|
||||
}
|
||||
value={fmt.dateTime(entry.performedAt)}
|
||||
/>
|
||||
)}
|
||||
@@ -147,7 +244,13 @@ function HistoryDetailModal({ config }) {
|
||||
{entry.dueDate && (
|
||||
<DetailRow
|
||||
icon={<CalendarMonth sx={{ fontSize: 16 }} />}
|
||||
label={entry.status === 6 ? 'Previous due date' : entry.status === 5 ? 'Was due' : 'Due date'}
|
||||
label={
|
||||
entry.status === 6
|
||||
? 'Previous due date'
|
||||
: entry.status === 5
|
||||
? 'Was due'
|
||||
: 'Due date'
|
||||
}
|
||||
value={fmt.dateTime(entry.dueDate)}
|
||||
/>
|
||||
)}
|
||||
@@ -184,45 +287,19 @@ function HistoryDetailModal({ config }) {
|
||||
<>
|
||||
<Divider />
|
||||
<Box sx={{ pt: 1 }}>
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary', mb: 0.5 }}>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.tertiary', mb: 0.5 }}
|
||||
>
|
||||
{entry.status === 2 || entry.status === 4 ? 'Reason' : 'Notes'}
|
||||
</Typography>
|
||||
<Box sx={{ overflowY: 'auto', maxHeight: '60vh' }}>
|
||||
<RichTextEditor value={entry.notes || ''} isEditable={false} />
|
||||
</Box>
|
||||
<Box sx={{ overflowY: 'auto', maxHeight: '60vh' }}>
|
||||
<RichTextEditor value={entry.notes || ''} isEditable={false} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Action buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 2, justifyContent: 'flex-end' }}>
|
||||
{entry.choreId && (
|
||||
<Button
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
startDecorator={<OpenInNew sx={{ fontSize: 16 }} />}
|
||||
onClick={() => {
|
||||
config?.onClose?.()
|
||||
navigate(`/chores/${entry.choreId}`)
|
||||
}}
|
||||
>
|
||||
Open Task
|
||||
</Button>
|
||||
)}
|
||||
{config?.onEdit && (
|
||||
<Button
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='md'
|
||||
startDecorator={<Edit sx={{ fontSize: 16 }} />}
|
||||
onClick={() => config.onEdit(entry)}
|
||||
>
|
||||
Edit Entry
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Button, Typography } from '@mui/joy'
|
||||
import { Typography } from '@mui/joy'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function AcknowledgmentModal({ config }) {
|
||||
@@ -11,42 +12,24 @@ function AcknowledgmentModal({ config }) {
|
||||
config.onClose()
|
||||
}, [config])
|
||||
|
||||
// Keyboard shortcuts for acknowledgment modal
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if (!config?.isOpen) return
|
||||
|
||||
// Show keyboard shortcuts when Ctrl/Cmd is pressed
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setShowKeyboardShortcuts(true)
|
||||
}
|
||||
if (event.ctrlKey || event.metaKey) setShowKeyboardShortcuts(true)
|
||||
|
||||
// Ctrl/Cmd + Y for acknowledge
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'y') {
|
||||
if (
|
||||
((event.ctrlKey || event.metaKey) && event.key === 'y') ||
|
||||
event.key === 'Escape' ||
|
||||
event.key === 'Enter'
|
||||
) {
|
||||
event.preventDefault()
|
||||
handleAction()
|
||||
return
|
||||
}
|
||||
|
||||
// Escape key for acknowledge
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
handleAction()
|
||||
return
|
||||
}
|
||||
|
||||
// Enter key for acknowledge
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleAction()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = event => {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
setShowKeyboardShortcuts(false)
|
||||
}
|
||||
if (!event.ctrlKey && !event.metaKey) setShowKeyboardShortcuts(false)
|
||||
}
|
||||
|
||||
if (config?.isOpen) {
|
||||
@@ -63,43 +46,33 @@ function AcknowledgmentModal({ config }) {
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={config?.isOpen}
|
||||
onClose={config?.onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
unmountDelay={250}
|
||||
onClose={handleAction}
|
||||
size='sm'
|
||||
title={config?.title}
|
||||
>
|
||||
<Box
|
||||
sx={{ p: 2, minWidth: { xs: '100%', sm: '400px' }, maxWidth: '500px' }}
|
||||
>
|
||||
|
||||
<Typography
|
||||
level='body-md'
|
||||
mb={3}
|
||||
sx={{
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
showCloseButton={false}
|
||||
footer={
|
||||
<ModalActions
|
||||
primary={{
|
||||
label: config?.acknowledgeText,
|
||||
color: config?.color || 'primary',
|
||||
onClick: handleAction,
|
||||
endDecorator: showKeyboardShortcuts ? (
|
||||
<KeyboardShortcutHint shortcut='Y' />
|
||||
) : undefined,
|
||||
}}
|
||||
>
|
||||
{config?.message}
|
||||
</Typography>
|
||||
|
||||
<Box display={'flex'} justifyContent={'center'} mt={2}>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={handleAction}
|
||||
color={config?.color || 'primary'}
|
||||
fullWidth
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='Y' show={showKeyboardShortcuts} />
|
||||
}
|
||||
sx={{ minWidth: '120px' }}
|
||||
>
|
||||
{config?.acknowledgeText}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{config?.message}
|
||||
</Typography>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { Save } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Divider,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Box, Button, Chip, Divider, Input, Typography } from '@mui/joy'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import BottomSheetModal from '../../../components/common/BottomSheetModal'
|
||||
import AppModal from '../../../components/common/AppModal'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import FilterBuilderContent, {
|
||||
conditionsToSelections,
|
||||
defaultSelections,
|
||||
@@ -18,6 +12,8 @@ import { FILTER_COLORS } from '../../../utils/Colors'
|
||||
import { applyFilter } from '../../../utils/FilterEngine'
|
||||
import { useFilters } from '../../Filters/FilterQueries'
|
||||
|
||||
const EMPTY_FILTERS = []
|
||||
|
||||
const AdvancedFilterBuilder = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -33,7 +29,7 @@ const AdvancedFilterBuilder = ({
|
||||
const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value)
|
||||
const [selections, setSelections] = useState(defaultSelections())
|
||||
const [error, setError] = useState('')
|
||||
const { data: existedFilters = [] } = useFilters()
|
||||
const { data: existedFilters = EMPTY_FILTERS } = useFilters()
|
||||
|
||||
const filterNameExists = (name, excludeId = null) =>
|
||||
existedFilters.some(
|
||||
@@ -55,9 +51,12 @@ const AdvancedFilterBuilder = ({
|
||||
setSelections(defaultSelections())
|
||||
}
|
||||
setError('')
|
||||
}, [editingFilter, isOpen])
|
||||
}, [editingFilter, existedFilters, isOpen])
|
||||
|
||||
const conditions = useMemo(() => selectionsToConditions(selections), [selections])
|
||||
const conditions = useMemo(
|
||||
() => selectionsToConditions(selections),
|
||||
[selections],
|
||||
)
|
||||
|
||||
const previewChores = useMemo(() => {
|
||||
if (conditions.length === 0) return []
|
||||
@@ -100,8 +99,9 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
<AppModal
|
||||
open={isOpen}
|
||||
isMobile
|
||||
onClose={onClose}
|
||||
maxHeight='92vh'
|
||||
title={
|
||||
@@ -109,7 +109,8 @@ const AdvancedFilterBuilder = ({
|
||||
{editingFilter ? 'Edit Filter' : 'New Filter'}
|
||||
{activeConditionCount > 0 && (
|
||||
<Chip size='sm' variant='solid' color='primary'>
|
||||
{activeConditionCount} condition{activeConditionCount !== 1 ? 's' : ''}
|
||||
{activeConditionCount} condition
|
||||
{activeConditionCount !== 1 ? 's' : ''}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
@@ -144,20 +145,19 @@ const AdvancedFilterBuilder = ({
|
||||
</Box>
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant='plain' color='neutral' size='sm' onClick={onClose}>
|
||||
<ModalActions>
|
||||
<Button variant='outlined' color='neutral' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
size='sm'
|
||||
startDecorator={<Save sx={{ fontSize: 16 }} />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Filter
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalActions>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
@@ -231,7 +231,7 @@ const AdvancedFilterBuilder = ({
|
||||
projects={projects}
|
||||
/>
|
||||
</Box>
|
||||
</BottomSheetModal>
|
||||
</AppModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AttachFile, Close, Image } from '@mui/icons-material'
|
||||
import { AttachFile, Image } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { GetChoreAttachments } from '../../../utils/Fetcher'
|
||||
import { resolvePhotoURL } from '../../../utils/Helpers'
|
||||
@@ -87,16 +87,7 @@ function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
||||
onClose={handleClose}
|
||||
title='Attachments'
|
||||
footer={
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
startDecorator={<Close />}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
<ModalActions primary={{ label: 'Done', onClick: handleClose }} />
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Close, Download } from '@mui/icons-material'
|
||||
import { Box, Button, CircularProgress, Typography } from '@mui/joy'
|
||||
import { Download } from '@mui/icons-material'
|
||||
import { Box, CircularProgress, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
const openUrl = async url => {
|
||||
@@ -47,25 +48,15 @@ function AttachmentViewerModal({ config }) {
|
||||
title={fileName || 'Attachment'}
|
||||
maxHeight='92vh'
|
||||
footer={
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
startDecorator={<Close />}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
startDecorator={<Download />}
|
||||
onClick={() => downloadUrl(url, fileName)}
|
||||
disabled={!url}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</Box>
|
||||
<ModalActions
|
||||
secondary={{ label: 'Close', onClick: handleClose }}
|
||||
primary={{
|
||||
label: 'Download',
|
||||
startDecorator: <Download />,
|
||||
onClick: () => downloadUrl(url, fileName),
|
||||
disabled: !url,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
@@ -78,10 +69,7 @@ function AttachmentViewerModal({ config }) {
|
||||
}}
|
||||
>
|
||||
{!imgLoaded && !imgError && (
|
||||
<CircularProgress
|
||||
sx={{ position: 'absolute' }}
|
||||
size='md'
|
||||
/>
|
||||
<CircularProgress sx={{ position: 'absolute' }} size='md' />
|
||||
)}
|
||||
{imgError ? (
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { CreateBackup, RestoreBackup } from '../../../utils/Fetcher'
|
||||
|
||||
@@ -140,7 +140,6 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
const response = await RestoreBackup(restoreEncryptionKey, backupData)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Backup restored successfully. Please refresh the page.',
|
||||
@@ -212,7 +211,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
placeholder='Enter a strong encryption key'
|
||||
/>
|
||||
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
||||
Keep this key safe - you'll need it to restore your backup
|
||||
Keep this key safe—you'll need it to restore your backup
|
||||
</Typography>
|
||||
</FormControl>
|
||||
|
||||
@@ -238,22 +237,6 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button size='lg' variant='outlined' onClick={handleClose} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
color='primary'
|
||||
onClick={handleCreateBackup}
|
||||
loading={loading}
|
||||
disabled={!encryptionKey.trim()}
|
||||
fullWidth
|
||||
>
|
||||
Create Backup
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -294,22 +277,6 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button size='lg' variant='outlined' onClick={handleClose} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
color='warning'
|
||||
onClick={handleRestore}
|
||||
loading={loading}
|
||||
disabled={!restoreEncryptionKey.trim() || !backupFile}
|
||||
fullWidth
|
||||
>
|
||||
Restore Backup
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -320,7 +287,28 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
unmountDelay={250}
|
||||
title='🔄 Backup & Restore'
|
||||
title='Backup & Restore'
|
||||
closeOnBackdrop={!loading}
|
||||
closeOnEscape={!loading}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
onClick: handleClose,
|
||||
disabled: loading,
|
||||
}}
|
||||
primary={{
|
||||
label: activeTab === 0 ? 'Create Backup' : 'Restore Backup',
|
||||
color: activeTab === 0 ? 'primary' : 'warning',
|
||||
onClick: activeTab === 0 ? handleCreateBackup : handleRestore,
|
||||
loading,
|
||||
disabled:
|
||||
activeTab === 0
|
||||
? !encryptionKey.trim()
|
||||
: !restoreEncryptionKey.trim() || !backupFile,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<Box
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Button, Typography } from '@mui/joy'
|
||||
import { Typography } from '@mui/joy'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function ConfirmationModal({ config }) {
|
||||
@@ -14,49 +15,29 @@ function ConfirmationModal({ config }) {
|
||||
[config],
|
||||
)
|
||||
|
||||
// Keyboard shortcuts for confirmation modal
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if (!config?.isOpen) return
|
||||
|
||||
// Show keyboard shortcuts when Ctrl/Cmd is pressed
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setShowKeyboardShortcuts(true)
|
||||
}
|
||||
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') {
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
|
||||
event.preventDefault()
|
||||
handleAction(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Escape key for cancel
|
||||
if (event.key === 'Escape') {
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
handleAction(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Enter key for confirm
|
||||
if (event.key === 'Enter') {
|
||||
} else if (event.key === 'Enter' && config?.color !== 'danger') {
|
||||
event.preventDefault()
|
||||
handleAction(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = event => {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
setShowKeyboardShortcuts(false)
|
||||
}
|
||||
if (!event.ctrlKey && !event.metaKey) setShowKeyboardShortcuts(false)
|
||||
}
|
||||
|
||||
if (config?.isOpen) {
|
||||
@@ -68,51 +49,45 @@ function ConfirmationModal({ config }) {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('keyup', handleKeyUp)
|
||||
}
|
||||
}, [config?.isOpen, handleAction])
|
||||
}, [config?.isOpen, config?.color, handleAction])
|
||||
|
||||
const isDestructive = config?.color === 'danger'
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={config?.isOpen}
|
||||
onClose={() => handleAction(false)}
|
||||
size='sm'
|
||||
unmountDelay={250}
|
||||
role={isDestructive ? 'alertdialog' : 'dialog'}
|
||||
title={config?.title}
|
||||
showCloseButton={false}
|
||||
closeOnBackdrop={!isDestructive}
|
||||
footer={
|
||||
<ModalActions
|
||||
stackOnMobile
|
||||
secondary={{
|
||||
label: config?.cancelText,
|
||||
onClick: () => handleAction(false),
|
||||
endDecorator: showKeyboardShortcuts ? (
|
||||
<KeyboardShortcutHint shortcut='X' />
|
||||
) : undefined,
|
||||
}}
|
||||
primary={{
|
||||
label: config?.confirmText,
|
||||
color: config?.color || 'primary',
|
||||
onClick: () => handleAction(true),
|
||||
endDecorator: showKeyboardShortcuts ? (
|
||||
<KeyboardShortcutHint shortcut='Y' />
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography level='h4' mb={1}>
|
||||
{config?.title}
|
||||
</Typography>
|
||||
<Typography level='body-md' gutterBottom>
|
||||
<Typography level='body-md' sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{config?.message}
|
||||
</Typography>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1} gap={1}>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
handleAction(true)
|
||||
}}
|
||||
fullWidth
|
||||
color={config?.color || 'primary'}
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='Y' show={showKeyboardShortcuts} />
|
||||
}
|
||||
>
|
||||
{config?.confirmText}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
handleAction(false)
|
||||
}}
|
||||
variant='outlined'
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='X' show={showKeyboardShortcuts} />
|
||||
}
|
||||
>
|
||||
{config?.cancelText}
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmationModal
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
@@ -104,16 +98,30 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
password === confirmPassword
|
||||
|
||||
return (
|
||||
<ResponsiveModal open={isOpen} onClose={handleClose}>
|
||||
<Typography level='h4' mb={2}>
|
||||
Create Sub Account
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md' mb={3}>
|
||||
Create a new sub account. The user will be able to log in using their
|
||||
combined username and complete tasks assigned to them.
|
||||
</Typography>
|
||||
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
title='Create Sub Account'
|
||||
description='Create a login that can complete tasks assigned to this account.'
|
||||
size='md'
|
||||
closeOnBackdrop={!isSubmitting}
|
||||
closeOnEscape={!isSubmitting}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
onClick: handleClose,
|
||||
disabled: isSubmitting,
|
||||
}}
|
||||
primary={{
|
||||
label: 'Create Account',
|
||||
onClick: handleSubmit,
|
||||
disabled: !isValid || isSubmitting,
|
||||
loading: isSubmitting,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormControl error={!!errors.childName} sx={{ mb: 2 }}>
|
||||
<Typography level='body2' mb={1}>
|
||||
Sub Account Name *
|
||||
@@ -196,27 +204,6 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
<FormHelperText>{errors.confirmPassword}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button
|
||||
size='lg'
|
||||
variant='outlined'
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Option,
|
||||
Select,
|
||||
Textarea,
|
||||
Typography,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Option,
|
||||
Select,
|
||||
Textarea,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
@@ -29,7 +28,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
setState(0)
|
||||
}
|
||||
}
|
||||
}, [type])
|
||||
}, [type, state])
|
||||
|
||||
const isValid = () => {
|
||||
const newErrors = {}
|
||||
@@ -63,9 +62,20 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
size='md'
|
||||
title={`${currentThing?.id ? 'Edit' : 'Create'} Thing`}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
onClick: onClose,
|
||||
}}
|
||||
primary={{
|
||||
label: currentThing?.id ? 'Update' : 'Create',
|
||||
onClick: handleSave,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography>Name</Typography>
|
||||
@@ -79,9 +89,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Typography>Type</Typography>
|
||||
<Select value={type} sx={{ minWidth: 300 }}>
|
||||
<Select value={type} onChange={(_, value) => setType(value)}>
|
||||
{['text', 'number', 'boolean'].map(type => (
|
||||
<Option value={type} key={type} onClick={() => setType(type)}>
|
||||
<Option value={type} key={type}>
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</Option>
|
||||
))}
|
||||
@@ -118,24 +128,15 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
{type === 'boolean' && (
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
<Select sx={{ minWidth: 300 }} value={state}>
|
||||
<Select value={state} onChange={(_, value) => setState(value)}>
|
||||
{['true', 'false'].map(value => (
|
||||
<Option value={value} key={value} onClick={() => setState(value)}>
|
||||
<Option value={value} key={value}>
|
||||
{value.charAt(0).toUpperCase() + value.slice(1)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button size='lg' onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{currentThing?.id ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} variant='outlined'>
|
||||
{currentThing?.id ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Box, Button, Input } from '@mui/joy'
|
||||
import { Input } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [date, setDate] = useState(
|
||||
current ? new Date(current).toISOString().split('T')[0] : '',
|
||||
)
|
||||
@@ -18,74 +18,23 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
size='sm'
|
||||
title={title}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: onClose }}
|
||||
primary={{ label: 'Save', onClick: handleSave, disabled: !date }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
sx={{ mt: 3 }}
|
||||
autoFocus
|
||||
type='date'
|
||||
value={date}
|
||||
onChange={e => setDate(e.target.value)}
|
||||
onChange={event => setDate(event.target.value)}
|
||||
/>
|
||||
|
||||
{/* <Box sx={{ mt: 3 }}>
|
||||
<Typography level='body-sm' sx={{ mb: 1.5, fontWeight: 500 }}>
|
||||
Quick select:
|
||||
</Typography>
|
||||
<Stack direction='row' spacing={1} flexWrap='wrap' useFlexGap>
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='primary'
|
||||
startDecorator={<Today />}
|
||||
size='lg'
|
||||
onClick={() => handleQuickSchedule('today')}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Today
|
||||
</Chip>
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='primary'
|
||||
startDecorator={<WbSunny />}
|
||||
size='lg'
|
||||
onClick={() => handleQuickSchedule('tomorrow')}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Tomorrow
|
||||
</Chip>
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='primary'
|
||||
startDecorator={<Weekend />}
|
||||
size='lg'
|
||||
onClick={() => handleQuickSchedule('weekend')}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Weekend
|
||||
</Chip>
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='primary'
|
||||
startDecorator={<NextWeek />}
|
||||
size='lg'
|
||||
onClick={() => handleQuickSchedule('next-week')}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Next week
|
||||
</Chip>
|
||||
</Stack>
|
||||
</Box> */}
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={4}>
|
||||
<Button size='lg' onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
Save
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateModal
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
@@ -31,7 +25,7 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
return
|
||||
}
|
||||
onSave({
|
||||
name,
|
||||
name: currentThing?.name,
|
||||
type: currentThing?.type,
|
||||
id: currentThing?.id,
|
||||
state: state || null,
|
||||
@@ -43,9 +37,14 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
size='sm'
|
||||
title='Update state'
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: onClose }}
|
||||
primary={{ label: 'Update', onClick: handleSave }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
@@ -57,15 +56,6 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
/>
|
||||
<FormHelperText color='danger'>{errors.state}</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button size='lg' onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{currentThing?.id ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} variant='outlined'>
|
||||
{currentThing?.id ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Avatar, Box, FormControl, FormLabel, Grid, Typography } from '@mui/joy'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { getTextColorFromBackgroundColor } from '../../../utils/Colors'
|
||||
import PROJECT_ICONS from '../../../utils/ProjectIcons'
|
||||
@@ -33,8 +26,10 @@ const IconPickerModal = ({
|
||||
fullWidth={true}
|
||||
unmountDelay={250}
|
||||
title='Choose Project Icon'
|
||||
footer={
|
||||
<ModalActions secondary={{ label: 'Cancel', onClick: onClose }} />
|
||||
}
|
||||
>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Available Icons</FormLabel>
|
||||
<Grid
|
||||
@@ -58,7 +53,9 @@ const IconPickerModal = ({
|
||||
border: '2px solid',
|
||||
borderColor: isCurrentIcon ? 'primary.500' : 'transparent',
|
||||
'&:hover': {
|
||||
borderColor: isCurrentIcon ? 'primary.600' : 'neutral.300',
|
||||
borderColor: isCurrentIcon
|
||||
? 'primary.600'
|
||||
: 'neutral.300',
|
||||
},
|
||||
transition: 'border-color 0.2s',
|
||||
}}
|
||||
@@ -96,12 +93,6 @@ const IconPickerModal = ({
|
||||
})}
|
||||
</Grid>
|
||||
</FormControl>
|
||||
|
||||
<Box display='flex' justifyContent='center' mt={3}>
|
||||
<Button variant='outlined' onClick={onClose} fullWidth size='lg'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Box, Button, FormControl, Input, Typography } from '@mui/joy'
|
||||
import { Box, FormControl, Input, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal.js'
|
||||
import { useNotification } from '../../../service/NotificationProvider.jsx'
|
||||
import LABEL_COLORS from '../../../utils/Colors.jsx'
|
||||
@@ -90,14 +91,13 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
fullWidth={true}
|
||||
title={label ? 'Edit Label' : 'Add Label'}
|
||||
footer={
|
||||
<Box display='flex' justifyContent='space-around' mt={1}>
|
||||
<Button size='lg' onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{label ? 'Save Changes' : 'Add Label'}
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: onClose }}
|
||||
primary={{
|
||||
label: label ? 'Save Changes' : 'Add Label',
|
||||
onClick: handleSave,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
@@ -120,12 +120,18 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{LABEL_COLORS.map(colorOption => (
|
||||
<Box
|
||||
component='button'
|
||||
type='button'
|
||||
key={colorOption.value}
|
||||
aria-label={`Select ${colorOption.name}`}
|
||||
aria-pressed={color === colorOption.value}
|
||||
title={colorOption.name}
|
||||
onClick={() => setColor(colorOption.value)}
|
||||
sx={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
width: 40,
|
||||
height: 40,
|
||||
border: 0,
|
||||
p: 0,
|
||||
borderRadius: '50%',
|
||||
background: colorOption.value,
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
import { Box, Button, Typography } from '@mui/joy'
|
||||
import { Box, Typography } from '@mui/joy'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
const NativeCancelSubscriptionModal = ({ isOpen, onClose }) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
return (
|
||||
<ResponsiveModal open={isOpen} onClose={onClose} size='md' fullWidth>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Cancel Subscription
|
||||
</Typography>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
title='Cancel Subscription'
|
||||
footer={
|
||||
<ModalActions
|
||||
stackOnMobile
|
||||
tertiary={{ label: 'Dismiss', onClick: onClose }}
|
||||
secondary={{
|
||||
label: "I'll cancel from my app store",
|
||||
onClick: onClose,
|
||||
}}
|
||||
primary={{
|
||||
label: 'Cancel desktop subscription',
|
||||
color: 'danger',
|
||||
onClick: () => onClose('desktop'),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Typography level='body-md' mb={3}>
|
||||
To cancel your subscription, please follow the instructions for your
|
||||
platform (you should cancel through the same platform you used to
|
||||
@@ -84,8 +102,8 @@ const NativeCancelSubscriptionModal = ({ isOpen, onClose }) => {
|
||||
<strong>Important:</strong> You must cancel your subscription
|
||||
through the same platform where you originally subscribed. If you
|
||||
subscribed through the iOS App Store or Google Play Store (even if
|
||||
you're now using the web/desktop version), you must cancel through
|
||||
that original platform using the instructions above.
|
||||
you're now using the web/desktop version), you must cancel
|
||||
through that original platform using the instructions above.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -93,24 +111,6 @@ const NativeCancelSubscriptionModal = ({ isOpen, onClose }) => {
|
||||
Your subscription will remain active until the end of your current
|
||||
billing period.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Button size='lg' onClick={onClose} variant='outlined' fullWidth>
|
||||
I'll cancel from my app store
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => onClose('desktop')}
|
||||
variant='solid'
|
||||
color='danger'
|
||||
fullWidth
|
||||
>
|
||||
I subscribed via desktop - Cancel now
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} fullWidth>
|
||||
Dismiss
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Textarea,
|
||||
Typography,
|
||||
Alert,
|
||||
Box,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Textarea,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { isOfficialDonetickInstanceSync } from '../../../utils/FeatureToggle'
|
||||
|
||||
@@ -108,19 +108,34 @@ function NudgeModal({ config }) {
|
||||
fullWidth={true}
|
||||
unmountDelay={250}
|
||||
title='Send Nudge'
|
||||
description='Send a gentle reminder to the people assigned to this task.'
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
onClick: () => handleAction(false),
|
||||
endDecorator: showKeyboardShortcuts ? (
|
||||
<KeyboardShortcutHint shortcut='X' />
|
||||
) : undefined,
|
||||
}}
|
||||
primary={{
|
||||
label: 'Send Nudge',
|
||||
onClick: () => handleAction(true),
|
||||
disabled: !isOfficialInstance,
|
||||
endDecorator: showKeyboardShortcuts ? (
|
||||
<KeyboardShortcutHint shortcut='Y' />
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography level='body-md' mb={2}>
|
||||
Send a gentle reminder to the assignee about this task. You can
|
||||
customize the message and choose who gets notified.
|
||||
</Typography>
|
||||
|
||||
{!isOfficialInstance && (
|
||||
<Alert color='warning' sx={{ mb: 2 }}>
|
||||
<Typography level='body-sm'>
|
||||
<strong>Heads up!</strong>This feature avaiable on Donetick Cloud!
|
||||
Since you'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.
|
||||
Since you'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
|
||||
@@ -152,33 +167,6 @@ function NudgeModal({ config }) {
|
||||
onChange={e => setNotifyAllAssignees(e.target.checked)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} gap={1}>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => handleAction(true)}
|
||||
disabled={!isOfficialInstance}
|
||||
fullWidth
|
||||
color='primary'
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='Y' show={showKeyboardShortcuts} />
|
||||
}
|
||||
>
|
||||
Send Nudge
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => handleAction(false)}
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='X' show={showKeyboardShortcuts} />
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect } from 'react'
|
||||
import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function PassowrdChangeModal({ isOpen, onClose }) {
|
||||
function PasswordChangeModal({ isOpen, onClose }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [passwordError, setPasswordError] = useState(null)
|
||||
const [passwordTouched, setPasswordTouched] = useState(false)
|
||||
const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false)
|
||||
|
||||
const [password, setPassword] = React.useState('')
|
||||
const [confirmPassword, setConfirmPassword] = React.useState('')
|
||||
const [passwordError, setPasswordError] = React.useState(false)
|
||||
const [passwordTouched, setPasswordTouched] = React.useState(false)
|
||||
const [confirmPasswordTouched, setConfirmPasswordTouched] =
|
||||
React.useState(false)
|
||||
useEffect(() => {
|
||||
if (!passwordTouched || !confirmPasswordTouched) {
|
||||
return
|
||||
} else if (password !== confirmPassword) {
|
||||
if (!passwordTouched || !confirmPasswordTouched) return
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setPasswordError('Passwords do not match')
|
||||
} else if (password.length < 8) {
|
||||
setPasswordError('Password must be at least 8 characters')
|
||||
@@ -32,90 +25,66 @@ function PassowrdChangeModal({ isOpen, onClose }) {
|
||||
}
|
||||
}, [password, confirmPassword, passwordTouched, confirmPasswordTouched])
|
||||
|
||||
const handleAction = isConfirmed => {
|
||||
if (!isConfirmed) {
|
||||
onClose(null)
|
||||
return
|
||||
}
|
||||
onClose(password)
|
||||
}
|
||||
const handleAction = isConfirmed => onClose(isConfirmed ? password : null)
|
||||
const canSubmit =
|
||||
passwordTouched &&
|
||||
confirmPasswordTouched &&
|
||||
password.length >= 8 &&
|
||||
password === confirmPassword &&
|
||||
passwordError == null
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
onClose={() => handleAction(false)}
|
||||
size='sm'
|
||||
title='Change Password'
|
||||
description='Choose a password between 8 and 64 characters.'
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: () => handleAction(false) }}
|
||||
primary={{
|
||||
label: 'Change Password',
|
||||
disabled: !canSubmit,
|
||||
onClick: () => handleAction(true),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography level='body-md' gutterBottom>
|
||||
Please enter your new password.
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Typography level='body2' alignSelf={'start'}>
|
||||
New Password
|
||||
</Typography>
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
<Typography level='body-sm'>New password</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
name='password'
|
||||
label='Password'
|
||||
type='password'
|
||||
id='password'
|
||||
placeholder='Enter password (8-64 characters)'
|
||||
autoComplete='new-password'
|
||||
placeholder='Enter password'
|
||||
value={password}
|
||||
onChange={e => {
|
||||
onChange={event => {
|
||||
setPasswordTouched(true)
|
||||
setPassword(e.target.value)
|
||||
setPassword(event.target.value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<Typography level='body2' alignSelf={'start'}>
|
||||
Confirm Password
|
||||
</Typography>
|
||||
<FormControl error={Boolean(passwordError)}>
|
||||
<Typography level='body-sm'>Confirm password</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
name='confirmPassword'
|
||||
label='confirmPassword'
|
||||
type='password'
|
||||
id='confirmPassword'
|
||||
autoComplete='new-password'
|
||||
placeholder='Repeat password'
|
||||
value={confirmPassword}
|
||||
onChange={e => {
|
||||
onChange={event => {
|
||||
setConfirmPasswordTouched(true)
|
||||
setConfirmPassword(e.target.value)
|
||||
setConfirmPassword(event.target.value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormHelperText>{passwordError}</FormHelperText>
|
||||
{passwordError && <FormHelperText>{passwordError}</FormHelperText>}
|
||||
</FormControl>
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button
|
||||
size='lg'
|
||||
disabled={passwordError != null}
|
||||
onClick={() => {
|
||||
handleAction(true)
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
handleAction(false)
|
||||
}}
|
||||
variant='outlined'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default PassowrdChangeModal
|
||||
|
||||
export default PasswordChangeModal
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import PROJECT_COLORS, {
|
||||
getTextColorFromBackgroundColor,
|
||||
@@ -124,28 +125,23 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
|
||||
unmountDelay={250}
|
||||
fullWidth={true}
|
||||
title={project ? 'Edit Project' : 'Create New Project'}
|
||||
closeOnBackdrop={!isSubmitting}
|
||||
closeOnEscape={!isSubmitting}
|
||||
footer={
|
||||
<Box display='flex' justifyContent='space-around' gap={1}>
|
||||
<Button
|
||||
type='submit'
|
||||
form='project-form'
|
||||
loading={isSubmitting}
|
||||
disabled={!projectName.trim() || isSubmitting}
|
||||
fullWidth
|
||||
size='lg'
|
||||
>
|
||||
{project ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
size='lg'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
<ModalActions
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
onClick: handleClose,
|
||||
disabled: isSubmitting,
|
||||
}}
|
||||
primary={{
|
||||
label: project ? 'Update' : 'Create',
|
||||
type: 'submit',
|
||||
form: 'project-form',
|
||||
loading: isSubmitting,
|
||||
disabled: !projectName.trim() || isSubmitting,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit} id='project-form'>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Button, Option, Select } from '@mui/joy'
|
||||
import React from 'react'
|
||||
import { Option, Select } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function SelectModal({
|
||||
@@ -12,8 +13,8 @@ function SelectModal({
|
||||
placeholder,
|
||||
}) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [selected, setSelected] = useState(null)
|
||||
|
||||
const [selected, setSelected] = React.useState(null)
|
||||
const handleSave = () => {
|
||||
onSave(options.find(item => item.id === selected))
|
||||
onClose()
|
||||
@@ -23,33 +24,33 @@ function SelectModal({
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
size='sm'
|
||||
title={title}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: onClose }}
|
||||
primary={{
|
||||
label: 'Save',
|
||||
onClick: handleSave,
|
||||
disabled: selected == null,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Select placeholder={placeholder}>
|
||||
{options.map((item, index) => (
|
||||
<Option
|
||||
value={item.id}
|
||||
key={item[displayKey]}
|
||||
onClick={() => {
|
||||
setSelected(item.id)
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
autoFocus
|
||||
placeholder={placeholder}
|
||||
value={selected}
|
||||
onChange={(_, value) => setSelected(value)}
|
||||
>
|
||||
{options.map(item => (
|
||||
<Option value={item.id} key={item[displayKey]}>
|
||||
{item[displayKey]}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button size='lg' onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
Save
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectModal
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Button, Textarea } from '@mui/joy'
|
||||
import { Textarea } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function TextModal({
|
||||
@@ -12,7 +13,6 @@ function TextModal({
|
||||
cancelText,
|
||||
}) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [text, setText] = useState(current)
|
||||
|
||||
const handleSave = () => {
|
||||
@@ -24,28 +24,25 @@ function TextModal({
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
size='md'
|
||||
title={title}
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: cancelText || 'Cancel', onClick: onClose }}
|
||||
primary={{ label: okText || 'Save', onClick: handleSave }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Textarea
|
||||
autoFocus
|
||||
placeholder='Type in here…'
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
minRows={2}
|
||||
maxRows={4}
|
||||
sx={{ minWidth: 300 }}
|
||||
onChange={event => setText(event.target.value)}
|
||||
minRows={3}
|
||||
maxRows={8}
|
||||
/>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button size='lg' onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{okText ? okText : 'Save'}
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} variant='outlined'>
|
||||
{cancelText ? cancelText : 'Cancel'}
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextModal
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocalization } from '../../../contexts/LocalizationContext'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useNotification } from '../../../service/NotificationProvider'
|
||||
import {
|
||||
@@ -59,7 +60,6 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
}
|
||||
}, [isOpen, timerData])
|
||||
|
||||
|
||||
const formatTime = seconds => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
@@ -304,10 +304,37 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
title='Timer Details'
|
||||
footer={
|
||||
<ModalActions
|
||||
tertiary={
|
||||
!loading && timerData && !editingSessions[timerData.id]
|
||||
? {
|
||||
label: 'Delete',
|
||||
color: 'danger',
|
||||
onClick: () => confirmDeleteSession(timerData.id),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
secondary={{ label: 'Close', onClick: handleClose }}
|
||||
primary={
|
||||
!loading && timerData
|
||||
? editingSessions[timerData.id]
|
||||
? {
|
||||
label: 'Save',
|
||||
onClick: () => saveSession(timerData.id),
|
||||
loading,
|
||||
}
|
||||
: {
|
||||
label: 'Edit',
|
||||
startDecorator: <Edit />,
|
||||
onClick: () => startEditingSession(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography level='h4'>Timer Details</Typography>
|
||||
|
||||
{loading && (
|
||||
<Alert color='neutral' sx={{ mb: 2 }}>
|
||||
Loading timer data...
|
||||
@@ -919,56 +946,6 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant='outlined' onClick={handleClose} color='neutral'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{/* Action buttons on the right */}
|
||||
{!loading && timerData && !editingSessions[timerData.id] && (
|
||||
<>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() => confirmDeleteSession(timerData.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
startDecorator={<Edit />}
|
||||
onClick={() => startEditingSession()}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Save button when editing */}
|
||||
{!loading && timerData && editingSessions[timerData.id] && (
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
onClick={() => saveSession(timerData.id)}
|
||||
loading={loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
|
||||
<ConfirmationModal config={confirmDeleteConfig} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
@@ -10,13 +9,13 @@ import {
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { data } from 'autoprefixer'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { CheckUserDeletion, DeleteUser } from '../../../utils/Fetcher'
|
||||
|
||||
function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
function UserDeletionModal({ isOpen, onClose }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const Navigate = useNavigate()
|
||||
const [step, setStep] = useState(1) // 1: Warning, 2: Transfer, 3: Confirm
|
||||
@@ -70,7 +69,8 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
setError(data.error || 'Failed to check deletion requirements')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(data.error || 'Failed to check deletion requirements')
|
||||
console.error('Failed to check deletion requirements:', err)
|
||||
setError('Failed to check deletion requirements')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -119,6 +119,7 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
setError(data.message || 'Failed to delete account')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete account:', err)
|
||||
setError('Failed to delete account')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -148,10 +149,6 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
|
||||
const renderWarningStep = () => (
|
||||
<>
|
||||
<Typography level='h4' mb={2} color='danger'>
|
||||
Delete Account
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md' mb={2}>
|
||||
<strong>This action cannot be undone.</strong> Deleting your account
|
||||
will permanently remove:
|
||||
@@ -193,30 +190,11 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' mt={3} gap={2}>
|
||||
<Button variant='outlined' onClick={() => handleClose(false)} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color='danger'
|
||||
onClick={checkDeletionRequirements}
|
||||
loading={loading}
|
||||
disabled={!password}
|
||||
fullWidth
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
const renderTransferStep = () => (
|
||||
<>
|
||||
<Typography level='h4' mb={2} color='warning'>
|
||||
Circle Ownership Transfer Required
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md' mb={3}>
|
||||
You own circles that require ownership transfer before deletion. Please
|
||||
select new owners:
|
||||
@@ -253,29 +231,11 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
</FormControl>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' mt={3} gap={2}>
|
||||
<Button variant='outlined' onClick={() => handleClose(false)} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={proceedToConfirmation}
|
||||
disabled={circlesRequiringTransfer.length !== transferOptions.length}
|
||||
fullWidth
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
const renderConfirmationStep = () => (
|
||||
<>
|
||||
<Typography level='h4' mb={2} color='danger'>
|
||||
Final Confirmation
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md' mb={3}>
|
||||
Please enter your password and type <strong>DELETE</strong> to confirm
|
||||
account deletion.
|
||||
@@ -296,7 +256,7 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ mb: 3 }}>
|
||||
<FormLabel>Type "DELETE" to confirm</FormLabel>
|
||||
<FormLabel>Type "DELETE" to confirm</FormLabel>
|
||||
<Input
|
||||
value={confirmation}
|
||||
onChange={e => setConfirmation(e.target.value)}
|
||||
@@ -309,21 +269,6 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button variant='outlined' onClick={() => handleClose(false)} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color='danger'
|
||||
onClick={executeUserDeletion}
|
||||
loading={loading}
|
||||
disabled={!password || confirmation !== 'DELETE'}
|
||||
fullWidth
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -345,8 +290,42 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
open={isOpen}
|
||||
onClose={() => handleClose(false)}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
title='Delete Account'
|
||||
title={
|
||||
step === 1
|
||||
? 'Delete Account'
|
||||
: step === 2
|
||||
? 'Transfer Circle Ownership'
|
||||
: 'Final Confirmation'
|
||||
}
|
||||
role={step === 3 ? 'alertdialog' : 'dialog'}
|
||||
closeOnBackdrop={false}
|
||||
footer={
|
||||
!loading && (
|
||||
<ModalActions
|
||||
stackOnMobile
|
||||
secondary={{
|
||||
label: 'Cancel',
|
||||
onClick: () => handleClose(false),
|
||||
}}
|
||||
primary={{
|
||||
label: step === 3 ? 'Delete Account' : 'Continue',
|
||||
color: step === 3 ? 'danger' : 'primary',
|
||||
onClick:
|
||||
step === 1
|
||||
? checkDeletionRequirements
|
||||
: step === 2
|
||||
? proceedToConfirmation
|
||||
: executeUserDeletion,
|
||||
disabled:
|
||||
step === 1
|
||||
? !password
|
||||
: step === 2
|
||||
? circlesRequiringTransfer.length !== transferOptions.length
|
||||
: !password || confirmation !== 'DELETE',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && step === 1 ? (
|
||||
<Box
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Avatar, Box, Button, List, ListItem, Typography } from '@mui/joy'
|
||||
import { Avatar, Box, List, ListItem, Typography } from '@mui/joy'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
@@ -11,6 +12,9 @@ const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
title='Select User'
|
||||
footer={
|
||||
<ModalActions secondary={{ label: 'Cancel', onClick: onClose }} />
|
||||
}
|
||||
>
|
||||
<List sx={{ mb: 2 }}>
|
||||
{performers.map(user => (
|
||||
@@ -38,11 +42,6 @@ const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button size='lg' variant='outlined' color='neutral' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,16 +5,9 @@ import {
|
||||
ErrorOutline,
|
||||
Nfc,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Input,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Box, IconButton, Input, Switch, Typography } from '@mui/joy'
|
||||
import { useRef, useState } from 'react'
|
||||
import ModalActions from '../../../components/common/ModalActions'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { startNativeNFCWrite } from '../../../service/NFCWriter'
|
||||
|
||||
@@ -29,9 +22,6 @@ const pulseKeyframes = `
|
||||
70% { transform: scale(2.1); opacity: 0; }
|
||||
100% { transform: scale(2.1); opacity: 0; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nfc-pulse-ring { animation: none !important; }
|
||||
}
|
||||
`
|
||||
|
||||
function NFCIcon({ status }) {
|
||||
@@ -55,7 +45,6 @@ function NFCIcon({ status }) {
|
||||
{isWaiting && (
|
||||
<>
|
||||
<Box
|
||||
className='nfc-pulse-ring'
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
@@ -63,10 +52,10 @@ function NFCIcon({ status }) {
|
||||
border: '2px solid',
|
||||
borderColor: 'primary.400',
|
||||
animation: 'nfc-pulse 1.8s ease-out infinite',
|
||||
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
className='nfc-pulse-ring'
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
@@ -74,6 +63,7 @@ function NFCIcon({ status }) {
|
||||
border: '2px solid',
|
||||
borderColor: 'primary.300',
|
||||
animation: 'nfc-pulse-2 1.8s ease-out infinite 0.4s',
|
||||
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -215,27 +205,37 @@ function WriteNFCModal({ config }) {
|
||||
return (
|
||||
<>
|
||||
<style>{pulseKeyframes}</style>
|
||||
<ResponsiveModal open={config?.isOpen} onClose={handleClose}>
|
||||
<ResponsiveModal
|
||||
open={config?.isOpen}
|
||||
onClose={handleClose}
|
||||
title={title}
|
||||
description={subtitle}
|
||||
closeOnBackdrop={!isWaiting}
|
||||
closeOnEscape={!isWaiting}
|
||||
footer={
|
||||
isSuccess ? (
|
||||
<ModalActions primary={{ label: 'Done', onClick: handleClose }} />
|
||||
) : isWaiting ? (
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: handleCancel }}
|
||||
/>
|
||||
) : (
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: handleClose }}
|
||||
primary={{
|
||||
label: nfcStatus === 'writing' ? 'Starting…' : 'Write tag',
|
||||
onClick: writeToNFC,
|
||||
disabled: nfcStatus === 'writing',
|
||||
startDecorator: <Nfc />,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box sx={{ px: 0.5, pb: 1 }}>
|
||||
{/* Icon */}
|
||||
<NFCIcon status={nfcStatus} />
|
||||
|
||||
{/* Heading */}
|
||||
<Typography
|
||||
level='title-lg'
|
||||
textAlign='center'
|
||||
sx={{ mb: 0.75, fontWeight: 600 }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
textAlign='center'
|
||||
sx={{ color: 'text.secondary', mb: 3, px: 2 }}
|
||||
>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
|
||||
{/* Idle / Error: URL + toggle + CTA */}
|
||||
{!isWaiting && !isSuccess && (
|
||||
<>
|
||||
@@ -264,6 +264,7 @@ function WriteNFCModal({ config }) {
|
||||
}}
|
||||
endDecorator={
|
||||
<IconButton
|
||||
aria-label='Copy tag URL'
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color={copied ? 'success' : 'neutral'}
|
||||
@@ -310,55 +311,8 @@ function WriteNFCModal({ config }) {
|
||||
size='sm'
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
||||
<Button
|
||||
size='lg'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
sx={{ flex: 1 }}
|
||||
onClick={isError ? handleClose : handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
sx={{ flex: 1 }}
|
||||
onClick={writeToNFC}
|
||||
disabled={nfcStatus === 'writing'}
|
||||
startDecorator={
|
||||
nfcStatus === 'writing' ? (
|
||||
<CircularProgress size='sm' />
|
||||
) : (
|
||||
<Nfc />
|
||||
)
|
||||
}
|
||||
>
|
||||
{nfcStatus === 'writing' ? 'Starting…' : 'Write tag'}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Waiting state */}
|
||||
{isWaiting && (
|
||||
<Button
|
||||
size='lg'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
fullWidth
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Success state */}
|
||||
{isSuccess && (
|
||||
<Button size='lg' fullWidth onClick={handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
</>
|
||||
|
||||
@@ -2,10 +2,8 @@ import { CreditCard, Person, Toll } from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
@@ -15,6 +13,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import ModalActions from '../../components/common/ModalActions.jsx'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal.js'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
|
||||
|
||||
@@ -53,22 +52,28 @@ function RedeemPointsModal({ config }) {
|
||||
const canRedeem = points > 0 && points <= config.available
|
||||
|
||||
return (
|
||||
<ResponsiveModal open={config?.isOpen} onClose={config?.onClose} size='md'>
|
||||
{/* Header Section */}
|
||||
<ResponsiveModal
|
||||
open={config?.isOpen}
|
||||
onClose={config?.onClose}
|
||||
size='md'
|
||||
title='Redeem Points'
|
||||
footer={
|
||||
<ModalActions
|
||||
secondary={{ label: 'Cancel', onClick: config?.onClose }}
|
||||
primary={{
|
||||
label: 'Redeem',
|
||||
startDecorator: <CreditCard />,
|
||||
disabled: !canRedeem,
|
||||
onClick: () =>
|
||||
config?.onSave({
|
||||
points: Number(points),
|
||||
userId: config?.user?.userId,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<CreditCard
|
||||
sx={{
|
||||
fontSize: '1.5rem',
|
||||
}}
|
||||
/>
|
||||
<Typography level='h4' sx={{ fontWeight: 600 }}>
|
||||
Redeem Points
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* User Info Card */}
|
||||
<Card
|
||||
variant='soft'
|
||||
@@ -155,6 +160,7 @@ function RedeemPointsModal({ config }) {
|
||||
{predefinedPoints.map(point => (
|
||||
<IconButton
|
||||
key={point}
|
||||
aria-label={`Add ${point} points`}
|
||||
variant='outlined'
|
||||
disabled={points + point > config?.available}
|
||||
onClick={() => addPredefinedPoints(point)}
|
||||
@@ -209,43 +215,6 @@ function RedeemPointsModal({ config }) {
|
||||
</Typography>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack direction='row' spacing={2}>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={config?.onClose}
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
fullWidth
|
||||
sx={{
|
||||
'&:hover': {
|
||||
backgroundColor: 'neutral.50',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() =>
|
||||
config?.onSave({
|
||||
points: Number(points),
|
||||
userId: config?.user?.userId,
|
||||
})
|
||||
}
|
||||
disabled={!canRedeem}
|
||||
fullWidth
|
||||
startDecorator={<CreditCard />}
|
||||
sx={{
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
Redeem
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user