Add IconButtonWithMenu component with label support

Add LearnMoreButton with some instruction on task modal
This commit is contained in:
Mo Tarbin 2025-01-15 01:54:28 -05:00
commit c8ac925146
4 changed files with 266 additions and 142 deletions

View file

@ -1,9 +1,10 @@
import { Chip, Menu, MenuItem, Typography } from '@mui/joy' import { Button, Chip, Menu, MenuItem, Typography } from '@mui/joy'
import IconButton from '@mui/joy/IconButton' import IconButton from '@mui/joy/IconButton'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
const IconButtonWithMenu = ({ const IconButtonWithMenu = ({
label,
key, key,
icon, icon,
options, options,
@ -39,6 +40,7 @@ const IconButtonWithMenu = ({
return ( return (
<> <>
{!label && (
<IconButton <IconButton
onClick={handleMenuOpen} onClick={handleMenuOpen}
variant='outlined' variant='outlined'
@ -50,7 +52,24 @@ const IconButtonWithMenu = ({
}} }}
> >
{icon} {icon}
{label ? label : null}
</IconButton> </IconButton>
)}
{label && (
<Button
onClick={handleMenuOpen}
variant='outlined'
color={isActive ? 'primary' : 'neutral'}
size='sm'
startDecorator={icon}
sx={{
height: 24,
borderRadius: 24,
}}
>
{label}
</Button>
)}
<Menu <Menu
key={key} key={key}

View file

@ -390,26 +390,25 @@ const MyChores = () => {
/> />
)} )}
{activeTextField != 'task' && ( {activeTextField != 'task' && (
<Button <IconButton
variant='outlined' variant='outlined'
size='sm' size='sm'
color='neutral' color='neutral'
sx={{ sx={{
height: 24, height: 24,
borderRadius: 24, borderRadius: 24,
minWidth: 100, // minWidth: 100,
}} }}
startDecorator={<EditCalendar />}
onClick={() => { onClick={() => {
setActiveTextField('task') setActiveTextField('task')
setTaskInputFocus(taskInputFocus + 1) setTaskInputFocus(taskInputFocus + 1)
}} }}
> >
Task <EditCalendar />
</Button> </IconButton>
)} )}
{activeTextField != 'search' && ( {activeTextField != 'search' && (
<Button <IconButton
variant='outlined' variant='outlined'
color='neutral' color='neutral'
size='sm' size='sm'
@ -417,7 +416,6 @@ const MyChores = () => {
height: 24, height: 24,
borderRadius: 24, borderRadius: 24,
}} }}
startDecorator={<Search />}
onClick={() => { onClick={() => {
setActiveTextField('search') setActiveTextField('search')
setSearchInputFocus(searchInputFocus + 1) setSearchInputFocus(searchInputFocus + 1)
@ -429,13 +427,37 @@ const MyChores = () => {
searchInputRef.current.value?.length searchInputRef.current.value?.length
}} }}
> >
Search <Search />
</Button> </IconButton>
)} )}
<Divider orientation='vertical' /> <Divider orientation='vertical' />
<IconButtonWithMenu <IconButtonWithMenu
title='Group by'
icon={<Sort />}
options={[
{ name: 'Due Date', value: 'due_date' },
{ name: 'Priority', value: 'priority' },
{ name: 'Labels', value: 'labels' },
]}
selectedItem={selectedChoreSection}
onItemSelect={selected => {
const section = ChoresGrouper(selected.value, chores)
setChoreSections(section)
setSelectedChoreSection(selected.value)
setFilteredChores(chores)
setSelectedFilter('All')
}}
mouseClickHandler={handleMenuOutsideClick}
/>
</Box>
{activeTextField === 'search' && (
<div className='flex gap-4'>
<div className='grid flex-1 grid-cols-3 gap-4'>
<IconButtonWithMenu
label={' Priority'}
key={'icon-menu-labels-filter'}
icon={<PriorityHigh />} icon={<PriorityHigh />}
title='Filter by Priority'
options={Priorities} options={Priorities}
selectedItem={selectedFilter} selectedItem={selectedFilter}
onItemSelect={selected => { onItemSelect={selected => {
@ -444,26 +466,12 @@ const MyChores = () => {
mouseClickHandler={handleMenuOutsideClick} mouseClickHandler={handleMenuOutsideClick}
isActive={selectedFilter.startsWith('Priority: ')} isActive={selectedFilter.startsWith('Priority: ')}
/> />
<IconButtonWithMenu <IconButtonWithMenu
key={'icon-menu-labels-filter'}
label={' Labels'}
icon={<Style />} icon={<Style />}
// TODO : this need simplification we want to display both user labels and chore labels options={userLabels}
// that why we are merging them here.
// we also filter out the labels that user created as those will be part of user labels
title='Filter by Label'
options={[
...userLabels,
...chores
.map(c => c.labelsV2)
.flat()
.filter(l => l.created_by !== userProfile.id)
.map(l => {
// if user created it don't show it:
return {
...l,
name: l.name + ' (Shared Label)',
}
}),
]}
selectedItem={selectedFilter} selectedItem={selectedFilter}
onItemSelect={selected => { onItemSelect={selected => {
handleLabelFiltering({ label: selected }) handleLabelFiltering({ label: selected })
@ -472,9 +480,11 @@ const MyChores = () => {
mouseClickHandler={handleMenuOutsideClick} mouseClickHandler={handleMenuOutsideClick}
useChips useChips
/> />
<IconButton
<Button
onClick={handleFilterMenuOpen} onClick={handleFilterMenuOpen}
variant='outlined' variant='outlined'
startDecorator={<FilterAlt />}
color={ color={
selectedFilter && selectedFilter &&
FILTERS[selectedFilter] && FILTERS[selectedFilter] &&
@ -488,8 +498,9 @@ const MyChores = () => {
borderRadius: 24, borderRadius: 24,
}} }}
> >
<FilterAlt /> {' Other'}
</IconButton> </Button>
<List <List
orientation='horizontal' orientation='horizontal'
wrap wrap
@ -519,7 +530,9 @@ const MyChores = () => {
> >
{filter} {filter}
<Chip <Chip
color={selectedFilter === filter ? 'primary' : 'neutral'} color={
selectedFilter === filter ? 'primary' : 'neutral'
}
> >
{FILTERS[filter].length === 2 {FILTERS[filter].length === 2
? FILTERS[filter](chores, userProfile.id).length ? FILTERS[filter](chores, userProfile.id).length
@ -527,6 +540,7 @@ const MyChores = () => {
</Chip> </Chip>
</MenuItem> </MenuItem>
))} ))}
{selectedFilter.startsWith('Label: ') || {selectedFilter.startsWith('Label: ') ||
(selectedFilter.startsWith('Priority: ') && ( (selectedFilter.startsWith('Priority: ') && (
<MenuItem <MenuItem
@ -541,26 +555,9 @@ const MyChores = () => {
))} ))}
</Menu> </Menu>
</List> </List>
<Divider orientation='vertical' /> </div>
<IconButtonWithMenu </div>
title='Group by' )}
icon={<Sort />}
options={[
{ name: 'Due Date', value: 'due_date' },
{ name: 'Priority', value: 'priority' },
{ name: 'Labels', value: 'labels' },
]}
selectedItem={selectedChoreSection}
onItemSelect={selected => {
const section = ChoresGrouper(selected.value, chores)
setChoreSections(section)
setSelectedChoreSection(selected.value)
setFilteredChores(chores)
setSelectedFilter('All')
}}
mouseClickHandler={handleMenuOutsideClick}
/>
</Box>
{selectedFilter !== 'All' && ( {selectedFilter !== 'All' && (
<Chip <Chip
level='title-md' level='title-md'

View file

@ -20,6 +20,7 @@ import { useNavigate } from 'react-router-dom'
import { CSSTransition } from 'react-transition-group' import { CSSTransition } from 'react-transition-group'
import { UserContext } from '../../contexts/UserContext' import { UserContext } from '../../contexts/UserContext'
import { CreateChore } from '../../utils/Fetcher' import { CreateChore } from '../../utils/Fetcher'
import LearnMoreButton from './LearnMore'
const VALID_DAYS = { const VALID_DAYS = {
monday: 'Monday', monday: 'Monday',
mon: 'Monday', mon: 'Monday',
@ -203,7 +204,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
{ {
frequencyType: 'days_of_the_week', frequencyType: 'days_of_the_week',
regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)$/i, regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)$/i,
name: 'Every {days} of the week', name: 'Every {days}',
}, },
{ {
frequencyType: 'day_of_the_month', frequencyType: 'day_of_the_month',
@ -314,7 +315,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
const handleTextChange = e => { const handleTextChange = e => {
if (!e.target.value) { if (!e.target.value) {
setTaskText('') setTaskText('')
setOpenModal(false)
setDueDate(null) setDueDate(null)
setFrequency(null) setFrequency(null)
setFrequencyHumanReadable(null) setFrequencyHumanReadable(null)
@ -327,6 +327,13 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
if (priority.result) setPriority(priority.result) if (priority.result) setPriority(priority.result)
cleanedSentence = priority.cleanedSentence cleanedSentence = priority.cleanedSentence
const repeat = parseRepeatV2(cleanedSentence)
if (repeat.result) {
setFrequency(repeat.result)
setFrequencyHumanReadable(repeat.name)
cleanedSentence = repeat.cleanedSentence
}
const parsedDueDate = chrono.parse(cleanedSentence, new Date(), { const parsedDueDate = chrono.parse(cleanedSentence, new Date(), {
forwardDate: true, forwardDate: true,
}) })
@ -337,13 +344,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '') cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '')
} }
const repeat = parseRepeatV2(cleanedSentence)
if (repeat.result) {
setFrequency(repeat.result)
setFrequencyHumanReadable(repeat.name)
cleanedSentence = repeat.cleanedSentence
}
if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) { if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) {
setOpenModal(true) setOpenModal(true)
} }
@ -425,7 +425,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
</Button> */} </Button> */}
<Typography level='h4'>Create new task</Typography> <Typography level='h4'>Create new task</Typography>
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'> <Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
Experimental Experimental Feature
</Chip> </Chip>
<Box> <Box>
<Typography level='body-sm'>Task in a sentence:</Typography> <Typography level='body-sm'>Task in a sentence:</Typography>
@ -438,13 +438,54 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
placeholder='Type your full text here...' placeholder='Type your full text here...'
sx={{ width: '100%', fontSize: '16px' }} sx={{ width: '100%', fontSize: '16px' }}
/> />
<LearnMoreButton
content={
<>
<Typography level='body-sm' sx={{ mb: 1 }}>
This feature lets you create a task simply by typing a
sentence. It attempt parses the sentence to identify the
task's due date, priority, and frequency.
</Typography>
<Typography
level='body-sm'
sx={{ fontWeight: 'bold', mt: 2 }}
>
Examples:
</Typography>
<Typography
level='body-sm'
component='ul'
sx={{ pl: 2, mt: 1, listStyle: 'disc' }}
>
<li>
<strong>Priority:</strong>For highest priority any of the
following keyword <em>P1</em>, <em>Urgent</em>,{' '}
<em>Important</em>, or <em>ASAP</em>. For lower
priorities, use <em>P2</em>, <em>P3</em>, or <em>P4</em>.
</li>
<li>
<strong>Due date:</strong> Specify dates with phrases like{' '}
<em>tomorrow</em>, <em>next week</em>, <em>Monday</em>, or{' '}
<em>August 1st at 12pm</em>.
</li>
<li>
<strong>Frequency:</strong> Set recurring tasks with terms
like <em>daily</em>, <em>weekly</em>, <em>monthly</em>,{' '}
<em>yearly</em>, or patterns such as{' '}
<em>every Tuesday and Thursday</em>.
</li>
</Typography>
</>
}
/>
</Box> </Box>
<Box> <Box>
<Typography level='body-sm'>Title:</Typography> <Typography level='body-sm'>Title:</Typography>
<Input <Input
value={taskTitle} value={taskTitle}
onChange={e => setTaskTitle(e.target.value)} onChange={e => setTaskTitle(e.target.value)}
placeholder='Type your full text here...'
sx={{ width: '100%', fontSize: '16px' }} sx={{ width: '100%', fontSize: '16px' }}
/> />
</Box> </Box>

View file

@ -0,0 +1,67 @@
import { Info } from '@mui/icons-material'
import { Box, Button, Sheet } from '@mui/joy'
import React, { useRef, useState } from 'react'
const LearnMoreButton = ({ content }) => {
const [open, setOpen] = useState(false)
const anchorRef = useRef(null)
const handleToggle = () => {
setOpen(prev => !prev)
}
const handleClickOutside = event => {
if (anchorRef.current && !anchorRef.current.contains(event.target)) {
setOpen(false)
}
}
React.useEffect(() => {
if (open) {
document.addEventListener('mousedown', handleClickOutside)
} else {
document.removeEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [open])
return (
<Box sx={{ position: 'relative', display: 'inline-block' }}>
<Button
ref={anchorRef}
variant='plain'
startDecorator={<Info />}
size='sm'
color='primary'
onClick={handleToggle}
>
Learn More
</Button>
{open && (
<Sheet
variant='outlined'
sx={{
position: 'absolute',
top: '100%',
left: 0,
mt: 1,
zIndex: 1000,
p: 2,
borderRadius: 'sm',
boxShadow: 'md',
backgroundColor: 'background.surface',
minWidth: 240,
maxHeight: 260,
overflowY: 'auto',
}}
>
{content}
</Sheet>
)}
</Box>
)
}
export default LearnMoreButton