Refactor modals to use ResponsiveModal for improved responsiveness and consistency
This commit is contained in:
@@ -63,7 +63,10 @@ const BottomSheetModal = forwardRef(
|
||||
if (internalOpen) {
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
// Prevent body scroll when modal is open
|
||||
document.body.style.overflow = 'hidden'
|
||||
// document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
// Restore scroll immediately when modal starts closing
|
||||
// document.body.style.overflow = 'unset'
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -95,6 +98,7 @@ const BottomSheetModal = forwardRef(
|
||||
ref={ref}
|
||||
sx={{
|
||||
zIndex: Z_INDEX.MODAL_CONTENT,
|
||||
minHeight: '30%',
|
||||
width: '100%',
|
||||
height: currentHeight,
|
||||
maxHeight: isExpanded ? expandedHeight : maxHeight,
|
||||
@@ -185,7 +189,9 @@ const BottomSheetModal = forwardRef(
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: showHandle ? '0 20px 16px 20px' : '16px 20px 16px 20px',
|
||||
padding: showHandle
|
||||
? '0 20px 16px 20px'
|
||||
: '16px 20px 16px 20px',
|
||||
paddingRight: showCloseButton ? '60px' : '20px', // Add space for close button
|
||||
minHeight: 24,
|
||||
}}
|
||||
|
||||
41
src/hooks/useConfirmationModal.js
Normal file
41
src/hooks/useConfirmationModal.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
const useConfirmationModal = () => {
|
||||
const [confirmModalConfig, setConfirmModalConfig] = useState({})
|
||||
|
||||
const showConfirmation = (
|
||||
message,
|
||||
title,
|
||||
onConfirm,
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
color = 'primary',
|
||||
) => {
|
||||
setConfirmModalConfig({
|
||||
isOpen: true,
|
||||
message,
|
||||
title,
|
||||
confirmText,
|
||||
cancelText,
|
||||
color,
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed) {
|
||||
onConfirm()
|
||||
}
|
||||
setConfirmModalConfig({})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const hideConfirmation = () => {
|
||||
setConfirmModalConfig({})
|
||||
}
|
||||
|
||||
return {
|
||||
confirmModalConfig,
|
||||
showConfirmation,
|
||||
hideConfirmation,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfirmationModal
|
||||
18
src/hooks/useResponsiveModal.js
Normal file
18
src/hooks/useResponsiveModal.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import BottomSheetModal from '../components/common/BottomSheetModal'
|
||||
import FadeModal from '../components/common/FadeModal'
|
||||
import useWindowWidth from './useWindowWidth'
|
||||
|
||||
/**
|
||||
* Hook that returns the appropriate modal component based on screen size
|
||||
* @param {number} breakpoint - Screen width breakpoint to switch between modals (default: 768px)
|
||||
* @returns {Object} - { Modal: Component, isMobile: boolean }
|
||||
*/
|
||||
export const useResponsiveModal = (breakpoint = 768) => {
|
||||
const windowWidth = useWindowWidth()
|
||||
const isMobile = windowWidth <= breakpoint
|
||||
|
||||
return {
|
||||
ResponsiveModal: isMobile ? BottomSheetModal : FadeModal,
|
||||
isMobile,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Box, Button, FormLabel, Input, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FadeModal from '../../components/common/FadeModal'
|
||||
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
import ConfirmationModal from './Inputs/ConfirmationModal'
|
||||
|
||||
function EditHistoryModal({ config, historyRecord }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
useEffect(() => {
|
||||
setCompletedDate(
|
||||
moment(historyRecord.performedAt).format('YYYY-MM-DDTHH:mm'),
|
||||
@@ -22,7 +25,7 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
const [notes, setNotes] = useState(historyRecord.notes)
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
return (
|
||||
<FadeModal open={config?.isOpen} onClose={config?.onClose}>
|
||||
<ResponsiveModal open={config?.isOpen} onClose={config?.onClose}>
|
||||
<Typography level='h4' mb={1}>
|
||||
Edit History
|
||||
</Typography>
|
||||
@@ -106,7 +109,7 @@ function EditHistoryModal({ config, historyRecord }) {
|
||||
cancelText: 'Cancel',
|
||||
}}
|
||||
/>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default EditHistoryModal
|
||||
|
||||
358
src/views/Modals/Inputs/BackupRestoreModal.jsx
Normal file
358
src/views/Modals/Inputs/BackupRestoreModal.jsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { CreateBackup, RestoreBackup } from '../../../utils/Fetcher'
|
||||
|
||||
function BackupRestoreModal({ isOpen, onClose, showNotification }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// Backup state
|
||||
const [encryptionKey, setEncryptionKey] = useState('')
|
||||
const [backupName, setBackupName] = useState('')
|
||||
const [includeAssets, setIncludeAssets] = useState(true)
|
||||
|
||||
// Restore state
|
||||
const [restoreEncryptionKey, setRestoreEncryptionKey] = useState('')
|
||||
const [backupFile, setBackupFile] = useState(null)
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
const resetModal = useCallback(() => {
|
||||
setActiveTab(0)
|
||||
setEncryptionKey('')
|
||||
setBackupName('')
|
||||
setIncludeAssets(true)
|
||||
setRestoreEncryptionKey('')
|
||||
setBackupFile(null)
|
||||
setError('')
|
||||
setLoading(false)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
resetModal()
|
||||
onClose()
|
||||
}, [onClose, resetModal])
|
||||
|
||||
const downloadFile = (data, filename) => {
|
||||
const blob = new Blob([data], { type: 'application/octet-stream' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
if (!encryptionKey.trim()) {
|
||||
setError('Encryption key is required')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await CreateBackup(
|
||||
encryptionKey,
|
||||
includeAssets,
|
||||
backupName,
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.slice(0, 19)
|
||||
.replace(/:/g, '-')
|
||||
const filename = backupName
|
||||
? `${backupName}-${timestamp}.backup`
|
||||
: `donetick-backup-${timestamp}.backup`
|
||||
|
||||
// Download the backup file
|
||||
downloadFile(data.backup_data, filename)
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Backup created and downloaded successfully',
|
||||
})
|
||||
|
||||
handleClose()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || 'Failed to create backup')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to create backup')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileUpload = event => {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
setBackupFile(file)
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestore = async () => {
|
||||
if (!restoreEncryptionKey.trim()) {
|
||||
setError('Encryption key is required')
|
||||
return
|
||||
}
|
||||
|
||||
if (!backupFile) {
|
||||
setError('Please select a backup file')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async e => {
|
||||
try {
|
||||
const backupData = e.target.result
|
||||
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.',
|
||||
})
|
||||
|
||||
// Refresh the page after a short delay to allow user to see the message
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000)
|
||||
|
||||
handleClose()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || 'Failed to restore backup')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to restore backup')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
setError('Failed to read backup file')
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
reader.readAsText(backupFile)
|
||||
} catch (err) {
|
||||
setError('Failed to restore backup')
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if (!isOpen) return
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
handleClose()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [isOpen, handleClose])
|
||||
|
||||
const renderBackupTab = () => (
|
||||
<Box>
|
||||
<Typography level='body-md' mb={3}>
|
||||
Create an encrypted backup of your data. This backup will include all
|
||||
your chores, history, settings, and optionally your uploaded files.
|
||||
</Typography>
|
||||
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
<FormLabel>Encryption Key *</FormLabel>
|
||||
<Input
|
||||
type='password'
|
||||
value={encryptionKey}
|
||||
onChange={e => setEncryptionKey(e.target.value)}
|
||||
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
|
||||
</Typography>
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
<FormLabel>Backup Name (Optional)</FormLabel>
|
||||
<Input
|
||||
value={backupName}
|
||||
onChange={e => setBackupName(e.target.value)}
|
||||
placeholder='e.g., weekly-backup'
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ mb: 3 }}>
|
||||
<Checkbox
|
||||
checked={includeAssets}
|
||||
onChange={e => setIncludeAssets(e.target.checked)}
|
||||
label='Include uploaded files and assets'
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{error && (
|
||||
<Typography level='body-sm' color='danger' mb={2}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button variant='outlined' onClick={handleClose} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={handleCreateBackup}
|
||||
loading={loading}
|
||||
disabled={!encryptionKey.trim()}
|
||||
fullWidth
|
||||
>
|
||||
Create Backup
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const renderRestoreTab = () => (
|
||||
<Box>
|
||||
<Typography level='body-md' mb={3} color='warning'>
|
||||
<strong>Warning:</strong> Restoring a backup will replace all your
|
||||
current data. This action cannot be undone.
|
||||
</Typography>
|
||||
|
||||
<FormControl sx={{ mb: 2 }}>
|
||||
<FormLabel>Backup File *</FormLabel>
|
||||
<Input
|
||||
type='file'
|
||||
accept='.backup'
|
||||
onChange={handleFileUpload}
|
||||
ref={fileInputRef}
|
||||
/>
|
||||
{backupFile && (
|
||||
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
||||
Selected: {backupFile.name}
|
||||
</Typography>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ mb: 3 }}>
|
||||
<FormLabel>Encryption Key *</FormLabel>
|
||||
<Input
|
||||
type='password'
|
||||
value={restoreEncryptionKey}
|
||||
onChange={e => setRestoreEncryptionKey(e.target.value)}
|
||||
placeholder='Enter the encryption key used for this backup'
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{error && (
|
||||
<Typography level='body-sm' color='danger' mb={2}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box display='flex' justifyContent='space-between' gap={2}>
|
||||
<Button variant='outlined' onClick={handleClose} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={handleRestore}
|
||||
loading={loading}
|
||||
disabled={!restoreEncryptionKey.trim() || !backupFile}
|
||||
fullWidth
|
||||
>
|
||||
Restore Backup
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
size='md'
|
||||
unmountDelay={250}
|
||||
>
|
||||
{loading ? (
|
||||
<Box
|
||||
display='flex'
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
minHeight={200}
|
||||
>
|
||||
<CircularProgress />
|
||||
<Typography level='body-md' sx={{ ml: 2 }}>
|
||||
{activeTab === 0 ? 'Creating backup...' : 'Restoring backup...'}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Typography level='h4' mb={3}>
|
||||
🔄 Backup & Restore
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(event, newValue) => setActiveTab(newValue)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab>Create Backup</Tab>
|
||||
<Tab>Restore Backup</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={0}>{renderBackupTab()}</TabPanel>
|
||||
|
||||
<TabPanel value={1}>{renderRestoreTab()}</TabPanel>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackupRestoreModal
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Box, Button, Typography } from '@mui/joy'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function ConfirmationModal({ config }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||
|
||||
const handleAction = useCallback(
|
||||
@@ -70,7 +71,7 @@ function ConfirmationModal({ config }) {
|
||||
}, [config?.isOpen, handleAction])
|
||||
|
||||
return (
|
||||
<FadeModal
|
||||
<ResponsiveModal
|
||||
open={config?.isOpen}
|
||||
onClose={config?.onClose}
|
||||
size='sm'
|
||||
@@ -79,7 +80,6 @@ function ConfirmationModal({ config }) {
|
||||
<Typography level='h4' mb={1}>
|
||||
{config?.title}
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md' gutterBottom>
|
||||
{config?.message}
|
||||
</Typography>
|
||||
@@ -90,7 +90,7 @@ function ConfirmationModal({ config }) {
|
||||
handleAction(true)
|
||||
}}
|
||||
fullWidth
|
||||
color={config.color ? config.color : 'primary'}
|
||||
color={config?.color || 'primary'}
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='Y' show={showKeyboardShortcuts} />
|
||||
}
|
||||
@@ -110,7 +110,7 @@ function ConfirmationModal({ config }) {
|
||||
{config?.cancelText}
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default ConfirmationModal
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
|
||||
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [name, setName] = useState(currentThing?.name || '')
|
||||
const [type, setType] = useState(currentThing?.type || 'number')
|
||||
const [state, setState] = useState(currentThing?.state || '')
|
||||
@@ -58,7 +59,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose}>
|
||||
<Typography level='h4'>
|
||||
{currentThing?.id ? 'Edit' : 'Create'} Thing
|
||||
</Typography>
|
||||
@@ -131,7 +132,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
{currentThing?.id ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default CreateThingModal
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Box, Button, Input, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
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] : null,
|
||||
)
|
||||
@@ -13,7 +15,7 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose}>
|
||||
<Typography variant='h4'>{title}</Typography>
|
||||
<Input
|
||||
sx={{ mt: 3 }}
|
||||
@@ -29,7 +31,7 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default DateModal
|
||||
|
||||
@@ -8,15 +8,17 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal.js'
|
||||
import { useNotification } from '../../../service/NotificationProvider.jsx'
|
||||
import LABEL_COLORS from '../../../utils/Colors.jsx'
|
||||
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
||||
import { useLabels } from '../../Labels/LabelQueries'
|
||||
|
||||
function LabelModal({ isOpen, onClose, label }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [labelName, setLabelName] = useState('')
|
||||
const [color, setColor] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
@@ -89,7 +91,7 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose}>
|
||||
<Typography level='title-md' mb={1}>
|
||||
{label ? 'Edit Label' : 'Add Label'}
|
||||
</Typography>
|
||||
@@ -159,7 +161,7 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function PassowrdChangeModal({ isOpen, onClose }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [password, setPassword] = React.useState('')
|
||||
const [confirmPassword, setConfirmPassword] = React.useState('')
|
||||
const [passwordError, setPasswordError] = React.useState(false)
|
||||
@@ -39,7 +41,7 @@ function PassowrdChangeModal({ isOpen, onClose }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose}>
|
||||
<Typography level='h4' mb={1}>
|
||||
Change Password
|
||||
</Typography>
|
||||
@@ -108,7 +110,7 @@ function PassowrdChangeModal({ isOpen, onClose }) {
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default PassowrdChangeModal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, Option, Select, Typography } from '@mui/joy'
|
||||
import React from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function SelectModal({
|
||||
isOpen,
|
||||
@@ -11,6 +11,8 @@ function SelectModal({
|
||||
displayKey,
|
||||
placeholder,
|
||||
}) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [selected, setSelected] = React.useState(null)
|
||||
const handleSave = () => {
|
||||
onSave(options.find(item => item.id === selected))
|
||||
@@ -18,7 +20,7 @@ function SelectModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose}>
|
||||
<Typography variant='h4'>{title}</Typography>
|
||||
<Select placeholder={placeholder}>
|
||||
{options.map((item, index) => (
|
||||
@@ -42,7 +44,7 @@ function SelectModal({
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default SelectModal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, Textarea, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function TextModal({
|
||||
isOpen,
|
||||
@@ -11,6 +11,8 @@ function TextModal({
|
||||
okText,
|
||||
cancelText,
|
||||
}) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [text, setText] = useState(current)
|
||||
|
||||
const handleSave = () => {
|
||||
@@ -19,7 +21,7 @@ function TextModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose}>
|
||||
<Typography variant='h4'>{title}</Typography>
|
||||
<Textarea
|
||||
placeholder='Type in here…'
|
||||
@@ -38,7 +40,7 @@ function TextModal({
|
||||
{cancelText ? cancelText : 'Cancel'}
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
export default TextModal
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useNotification } from '../../../service/NotificationProvider'
|
||||
import {
|
||||
DeleteTimeSession,
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
import ConfirmationModal from './ConfirmationModal'
|
||||
|
||||
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [timerData, setTimerData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editingSessions, setEditingSessions] = useState({})
|
||||
@@ -310,7 +312,12 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FadeModal open={isOpen} onClose={onClose} size='lg' fullWidth={true}>
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
>
|
||||
<Typography level='h4'>Timer Details</Typography>
|
||||
|
||||
{loading && (
|
||||
@@ -976,7 +983,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
|
||||
<ConfirmationModal config={confirmDeleteConfig} />
|
||||
</>
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
import { data } from 'autoprefixer'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { CheckUserDeletion, DeleteUser } from '../../../utils/Fetcher'
|
||||
|
||||
function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const Navigate = useNavigate()
|
||||
const [step, setStep] = useState(1) // 1: Warning, 2: Transfer, 3: Confirm
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -340,11 +341,11 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal
|
||||
<ResponsiveModal
|
||||
open={isOpen}
|
||||
onClose={() => handleClose(false)}
|
||||
size='md'
|
||||
unmountDelay={250}
|
||||
title='Delete Account'
|
||||
>
|
||||
{loading && step === 1 ? (
|
||||
<Box
|
||||
@@ -358,7 +359,7 @@ function UserDeletionModal({ isOpen, onClose, userProfile }) {
|
||||
) : (
|
||||
renderStep()
|
||||
)}
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Avatar, Box, Button, List, ListItem, Typography } from '@mui/joy'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
return (
|
||||
<FadeModal open={isOpen} onClose={onClose} size='md' fullWidth>
|
||||
<ResponsiveModal open={isOpen} onClose={onClose} size='md' fullWidth>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Select User
|
||||
</Typography>
|
||||
@@ -38,7 +40,7 @@ const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { CopyAll } from '@mui/icons-material'
|
||||
import { Box, Button, Checkbox, Input, ListItem, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function WriteNFCModal({ config }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [nfcStatus, setNfcStatus] = useState('idle') // 'idle', 'writing', 'success', 'error'
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [isAutoCompleteWhenScan, setIsAutoCompleteWhenScan] = useState(false)
|
||||
@@ -52,7 +54,7 @@ function WriteNFCModal({ config }) {
|
||||
return url
|
||||
}
|
||||
return (
|
||||
<FadeModal open={config?.isOpen} onClose={handleClose}>
|
||||
<ResponsiveModal open={config?.isOpen} onClose={handleClose}>
|
||||
<Typography level='h4' mb={1}>
|
||||
{nfcStatus === 'success' ? 'Success!' : 'Write to NFC'}
|
||||
</Typography>
|
||||
@@ -106,7 +108,7 @@ function WriteNFCModal({ config }) {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FadeModal from '../../components/common/FadeModal'
|
||||
|
||||
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
|
||||
|
||||
function RedeemPointsModal({ config }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
const [points, setPoints] = useState(0)
|
||||
const predefinedPoints = [1, 5, 10, 25, 50]
|
||||
|
||||
@@ -50,7 +52,7 @@ function RedeemPointsModal({ config }) {
|
||||
const canRedeem = points > 0 && points <= config.available
|
||||
|
||||
return (
|
||||
<FadeModal open={config?.isOpen} onClose={config?.onClose} size='md'>
|
||||
<ResponsiveModal open={config?.isOpen} onClose={config?.onClose} size='md'>
|
||||
{/* Header Section */}
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
@@ -242,7 +244,7 @@ function RedeemPointsModal({ config }) {
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { FormControl } from '@mui/material'
|
||||
import * as chrono from 'chrono-node'
|
||||
import moment from 'moment'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import FadeModal from '../../components/common/FadeModal'
|
||||
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
||||
import { useCreateChore } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import {
|
||||
parseAssignees,
|
||||
parseDueDate,
|
||||
parseLabels,
|
||||
parsePriority,
|
||||
@@ -41,6 +42,7 @@ const getDefaultNotification = () => {
|
||||
}
|
||||
|
||||
const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||
const { data: circleMembers, isLoading: isCircleMembersLoading } =
|
||||
useCircleMembers()
|
||||
@@ -69,7 +71,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
const [hasDescription, setHasDescription] = useState(false)
|
||||
const [hasSubTasks, setHasSubTasks] = useState(false)
|
||||
const [hasNotifications, setHasNotifications] = useState(false)
|
||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(true)
|
||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||
|
||||
// set showKeyboardShortcuts true as soon as the user hold ctrl or cmd key:
|
||||
useEffect(() => {
|
||||
@@ -301,32 +303,34 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
cleanedSentence = repeat.cleanedSentence
|
||||
}
|
||||
// Parse assignees using circle members
|
||||
// const circleMembersList = circleMembers?.res || []
|
||||
// const assigneesForParsing = circleMembersList.map(member => ({
|
||||
// userId: member.userId,
|
||||
// username:
|
||||
// member.username ||
|
||||
// member.displayName?.toLowerCase().replace(/\s+/g, ''),
|
||||
// displayName: member.displayName,
|
||||
// name: member.displayName,
|
||||
// id: member.userId,
|
||||
// }))
|
||||
const circleMembersList = circleMembers?.res || []
|
||||
const assigneesForParsing = circleMembersList.map(member => ({
|
||||
userId: member.userId,
|
||||
username:
|
||||
member.username ||
|
||||
member.displayName?.toLowerCase().replace(/\s+/g, ''),
|
||||
displayName: member.displayName,
|
||||
name: member.displayName,
|
||||
id: member.userId,
|
||||
}))
|
||||
|
||||
// const assigneesResult = parseAssignees(sentence, assigneesForParsing)
|
||||
// if (assigneesResult.result) {
|
||||
// cleanedSentence = assigneesResult.cleanedSentence
|
||||
// setAssignees(
|
||||
// assigneesResult.result.map(assignee => ({
|
||||
// userId: assignee.userId,
|
||||
// })),
|
||||
// )
|
||||
// } else {
|
||||
// setAssignees([
|
||||
// {
|
||||
// userId: userProfile.id,
|
||||
// },
|
||||
// ])
|
||||
// }
|
||||
const assigneesResult = parseAssignees(sentence, assigneesForParsing)
|
||||
if (assigneesResult.result) {
|
||||
cleanedSentence = assigneesResult.cleanedSentence
|
||||
console.log('CLEANED', cleanedSentence)
|
||||
|
||||
setAssignees(
|
||||
assigneesResult.result.map(assignee => ({
|
||||
userId: assignee.userId,
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
setAssignees([
|
||||
{
|
||||
userId: userProfile.id,
|
||||
},
|
||||
])
|
||||
}
|
||||
// Parse due date
|
||||
const dueDateParsed = parseDueDate(sentence, chrono)
|
||||
let dueDateHighlight = null
|
||||
@@ -462,13 +466,13 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeModal
|
||||
<ResponsiveModal
|
||||
open={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
size='lg'
|
||||
fullWidth={true}
|
||||
title='Create new task'
|
||||
>
|
||||
<Typography level='h4'>Create new task</Typography>
|
||||
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
||||
Experimental Feature
|
||||
</Chip>
|
||||
@@ -772,7 +776,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user