feat: Enhance UserPoints component with improved filtering and layout
- Updated UserPoints component to include a more user-friendly filter bar with enhanced styling. - Added a summary section to display the current filter context. - Refactored user selection and time period filtering logic for better clarity and performance. - Improved the layout of points cards and history sections for better visual hierarchy. - Integrated a bar chart for visual representation of points over time. - Updated redeem points functionality with better user feedback. feat: Add keyboard shortcuts in AddTaskModal for improved usability - Implemented keyboard shortcuts for adding descriptions, subtasks, and due dates. - Enhanced user experience by providing visual hints for keyboard shortcuts. - Refactored task creation logic to streamline the process. fix: Refactor ChoreActionMenu to handle mouse events and improve accessibility - Added mouse enter and leave event handlers for better interaction feedback. - Adjusted menu positioning for improved usability. refactor: Update RichTextEditor to support focus handling from parent components - Converted RichTextEditor to use forwardRef for better integration with parent components. - Exposed focus and blur methods for external control. - Improved image upload handling with better error management. fix: Adjust SubTask component to handle Enter key behavior correctly - Modified key event handling to prevent unintended task creation when holding meta or ctrl keys. - Added autoFocus prop to new task input for better user experience.
This commit is contained in:
@@ -1,10 +1,73 @@
|
||||
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'
|
||||
|
||||
function ConfirmationModal({ config }) {
|
||||
const handleAction = isConfirmed => {
|
||||
config.onClose(isConfirmed)
|
||||
}
|
||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||
|
||||
const handleAction = useCallback(
|
||||
isConfirmed => {
|
||||
config.onClose(isConfirmed)
|
||||
},
|
||||
[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)
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + Y for confirm
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'y') {
|
||||
event.preventDefault()
|
||||
handleAction(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + X for cancel
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
|
||||
event.preventDefault()
|
||||
handleAction(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Escape key for cancel
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
handleAction(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Enter key for confirm
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleAction(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = event => {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
setShowKeyboardShortcuts(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (config?.isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('keyup', handleKeyUp)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('keyup', handleKeyUp)
|
||||
}
|
||||
}, [config?.isOpen, handleAction])
|
||||
|
||||
return (
|
||||
<FadeModal
|
||||
@@ -21,22 +84,28 @@ function ConfirmationModal({ config }) {
|
||||
{config?.message}
|
||||
</Typography>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1} gap={1}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleAction(true)
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ mr: 1 }}
|
||||
color={config.color ? config.color : 'primary'}
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='Y' show={showKeyboardShortcuts} />
|
||||
}
|
||||
>
|
||||
{config?.confirmText}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleAction(false)
|
||||
}}
|
||||
variant='outlined'
|
||||
endDecorator={
|
||||
<KeyboardShortcutHint shortcut='X' show={showKeyboardShortcuts} />
|
||||
}
|
||||
>
|
||||
{config?.cancelText}
|
||||
</Button>
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Input,
|
||||
ModalDialog,
|
||||
ModalClose,
|
||||
Box,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Box, Button, Input, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
|
||||
function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
const [date, setDate] = useState(
|
||||
@@ -20,26 +13,23 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={onClose}>
|
||||
<ModalDialog>
|
||||
{/* <ModalClose /> */}
|
||||
<Typography variant='h4'>{title}</Typography>
|
||||
<Input
|
||||
sx={{ mt: 3 }}
|
||||
type='date'
|
||||
value={date}
|
||||
onChange={e => setDate(e.target.value)}
|
||||
/>
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<Typography variant='h4'>{title}</Typography>
|
||||
<Input
|
||||
sx={{ mt: 3 }}
|
||||
type='date'
|
||||
value={date}
|
||||
onChange={e => setDate(e.target.value)}
|
||||
/>
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
)
|
||||
}
|
||||
export default DateModal
|
||||
|
||||
@@ -4,11 +4,10 @@ import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Modal,
|
||||
ModalDialog,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
|
||||
function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const [state, setState] = useState(currentThing?.state || '')
|
||||
@@ -39,31 +38,29 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={onClose}>
|
||||
<ModalDialog>
|
||||
<Typography level='h4'>Update state</Typography>
|
||||
<FadeModal open={isOpen} onClose={onClose}>
|
||||
<Typography level='h4'>Update state</Typography>
|
||||
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
value={state || ''}
|
||||
onChange={e => setState(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
<FormHelperText color='danger'>{errors.state}</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Typography>Value</Typography>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
value={state || ''}
|
||||
onChange={e => setState(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
<FormHelperText color='danger'>{errors.state}</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{currentThing?.id ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant='outlined'>
|
||||
{currentThing?.id ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button onClick={handleSave} fullWidth sx={{ mr: 1 }}>
|
||||
{currentThing?.id ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant='outlined'>
|
||||
{currentThing?.id ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
)
|
||||
}
|
||||
export default EditThingStateModal
|
||||
|
||||
@@ -345,104 +345,220 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
}}
|
||||
>
|
||||
{/* Active Time */}
|
||||
<Box
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'success.500',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
<Box
|
||||
sx={{
|
||||
color: 'success.600',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(calculateCurrentActiveDuration())}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Active Work
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'success.500',
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Active Work
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'success.600',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(calculateCurrentActiveDuration())}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Idle Time */}
|
||||
<Box
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'warning.500',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
<Box
|
||||
sx={{
|
||||
color: 'warning.600',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(calculateIdleTime())}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Break Time
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'warning.500',
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Break Time
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'warning.600',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(calculateIdleTime())}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Total Sessions */}
|
||||
<Box
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.500',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
<Box
|
||||
sx={{
|
||||
color: 'primary.600',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{timerData.pauseLog?.length || 0}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Work Sessions
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'primary.500',
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Work Sessions
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'primary.600',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{timerData.pauseLog?.length || 0}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Total Session Time */}
|
||||
<Box
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.500',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
<Box
|
||||
sx={{
|
||||
color: 'neutral.700',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatTime(calculateTotalDuration())}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Total Time
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'neutral.500',
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
Total Time
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'neutral.700',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{formatTime(calculateTotalDuration())}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Progress Bar */}
|
||||
|
||||
@@ -1,80 +1,249 @@
|
||||
import { Box, Button, FormLabel, IconButton, Input, Typography } from '@mui/joy'
|
||||
import { CreditCard, Person, Toll } from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Stack,
|
||||
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 [points, setPoints] = useState(0)
|
||||
const predefinedPoints = [1, 5, 10, 25, 50]
|
||||
|
||||
useEffect(() => {
|
||||
setPoints(0)
|
||||
}, [config])
|
||||
|
||||
const [points, setPoints] = useState(0)
|
||||
const handlePointsChange = value => {
|
||||
const numValue = Number(value)
|
||||
if (numValue > config.available) {
|
||||
setPoints(config.available)
|
||||
return
|
||||
}
|
||||
if (numValue < 0) {
|
||||
setPoints(0)
|
||||
return
|
||||
}
|
||||
setPoints(numValue)
|
||||
}
|
||||
|
||||
const predefinedPoints = [1, 5, 10, 25]
|
||||
const addPredefinedPoints = point => {
|
||||
const newPoints = points + point
|
||||
if (newPoints > config.available) {
|
||||
setPoints(config.available)
|
||||
return
|
||||
}
|
||||
setPoints(newPoints)
|
||||
}
|
||||
|
||||
const canRedeem = points > 0 && points <= config.available
|
||||
|
||||
return (
|
||||
<FadeModal open={config?.isOpen} onClose={config?.onClose}>
|
||||
<Typography level='h4' mb={1}>
|
||||
Redeem Points
|
||||
</Typography>
|
||||
<FormLabel>
|
||||
Points to Redeem ({config.available ? config.available : 0} points
|
||||
available)
|
||||
</FormLabel>
|
||||
<Input
|
||||
type='number'
|
||||
value={points}
|
||||
slotProps={{
|
||||
input: { min: 0, max: config.available ? config.available : 0 },
|
||||
}}
|
||||
onChange={e => {
|
||||
if (e.target.value > config.available) {
|
||||
setPoints(config.available)
|
||||
return
|
||||
}
|
||||
setPoints(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<FormLabel>Or select from predefined points:</FormLabel>
|
||||
<Box display='flex' justifyContent='space-evenly' mb={1}>
|
||||
{predefinedPoints.map(point => (
|
||||
<IconButton
|
||||
<FadeModal open={config?.isOpen} onClose={config?.onClose} size='md'>
|
||||
{/* Header Section */}
|
||||
<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'
|
||||
sx={{
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Stack direction='row' spacing={2} alignItems='center'>
|
||||
<Avatar
|
||||
size='md'
|
||||
src={resolvePhotoURL(config?.user?.image)}
|
||||
sx={{
|
||||
border: '2px solid',
|
||||
borderColor: 'warning.200',
|
||||
}}
|
||||
>
|
||||
<Person />
|
||||
</Avatar>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
||||
{config?.user?.displayName || 'User'}
|
||||
</Typography>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='success'
|
||||
startDecorator={<Toll />}
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
{config?.available || 0} points available
|
||||
</Chip>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Points Input Section */}
|
||||
<FormControl>
|
||||
<FormLabel sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Points to Redeem
|
||||
</FormLabel>
|
||||
<Input
|
||||
type='number'
|
||||
value={points}
|
||||
size='lg'
|
||||
variant='outlined'
|
||||
disabled={points + point > config.available}
|
||||
sx={{ borderRadius: '50%' }}
|
||||
key={point}
|
||||
onClick={() => {
|
||||
const newPoints = points + point
|
||||
if (newPoints > config.available) {
|
||||
setPoints(config.available)
|
||||
return
|
||||
}
|
||||
setPoints(newPoints)
|
||||
startDecorator={<Toll />}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0,
|
||||
max: config?.available || 0,
|
||||
placeholder: 'Enter points...',
|
||||
},
|
||||
}}
|
||||
onChange={e => handlePointsChange(e.target.value)}
|
||||
sx={{
|
||||
'--Input-decoratorChildHeight': '45px',
|
||||
fontSize: 'lg',
|
||||
fontWeight: 500,
|
||||
'&:focus-within': {
|
||||
borderColor: 'warning.500',
|
||||
boxShadow: '0 0 0 2px rgba(255, 193, 7, 0.2)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{points > config?.available && (
|
||||
<Typography level='body-xs' sx={{ color: 'danger.500', mt: 0.5 }}>
|
||||
Cannot exceed available points
|
||||
</Typography>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{/* Quick Selection Buttons */}
|
||||
<Box>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 600, mb: 1.5 }}>
|
||||
Quick Add:
|
||||
</Typography>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={1}
|
||||
justifyContent='center'
|
||||
flexWrap='wrap'
|
||||
useFlexGap
|
||||
>
|
||||
{predefinedPoints.map(point => (
|
||||
<IconButton
|
||||
key={point}
|
||||
variant='outlined'
|
||||
disabled={points + point > config?.available}
|
||||
onClick={() => addPredefinedPoints(point)}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 45,
|
||||
minHeight: 45,
|
||||
fontWeight: 600,
|
||||
fontSize: 'sm',
|
||||
'&:hover:not(:disabled)': {
|
||||
transform: 'scale(1.05)',
|
||||
boxShadow: 'sm',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
+{point}
|
||||
</IconButton>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Summary Section */}
|
||||
{points > 0 && (
|
||||
<Card
|
||||
variant='soft'
|
||||
color='primary'
|
||||
sx={{
|
||||
p: 2,
|
||||
textAlign: 'center',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(25,118,210,0.1) 0%, rgba(25,118,210,0.05) 100%)',
|
||||
}}
|
||||
>
|
||||
{point}
|
||||
</IconButton>
|
||||
))}
|
||||
</Box>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
You are about to redeem
|
||||
</Typography>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{ color: 'primary.600', fontWeight: 700 }}
|
||||
>
|
||||
{points} points
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.secondary', mt: 0.5 }}
|
||||
>
|
||||
Remaining: {(config?.available || 0) - points} points
|
||||
</Typography>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 3 button save , cancel and delete */}
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
config.onSave({
|
||||
points: Number(points),
|
||||
userId: config.user.userId,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Redeem
|
||||
</Button>
|
||||
<Button onClick={config.onClose} variant='outlined'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
<Divider />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack direction='row' spacing={2}>
|
||||
<Button
|
||||
onClick={config?.onClose}
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
fullWidth
|
||||
sx={{
|
||||
'&:hover': {
|
||||
backgroundColor: 'neutral.50',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
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>
|
||||
</FadeModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RedeemPointsModal
|
||||
|
||||
Reference in New Issue
Block a user