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,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