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 React, { useEffect, useRef, useState } from 'react'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
const IconButtonWithMenu = ({
label,
key,
icon,
options,
@ -39,18 +40,36 @@ const IconButtonWithMenu = ({
return (
<>
<IconButton
onClick={handleMenuOpen}
variant='outlined'
color={isActive ? 'primary' : 'neutral'}
size='sm'
sx={{
height: 24,
borderRadius: 24,
}}
>
{icon}
</IconButton>
{!label && (
<IconButton
onClick={handleMenuOpen}
variant='outlined'
color={isActive ? 'primary' : 'neutral'}
size='sm'
sx={{
height: 24,
borderRadius: 24,
}}
>
{icon}
{label ? label : null}
</IconButton>
)}
{label && (
<Button
onClick={handleMenuOpen}
variant='outlined'
color={isActive ? 'primary' : 'neutral'}
size='sm'
startDecorator={icon}
sx={{
height: 24,
borderRadius: 24,
}}
>
{label}
</Button>
)}
<Menu
key={key}

View file

@ -390,26 +390,25 @@ const MyChores = () => {
/>
)}
{activeTextField != 'task' && (
<Button
<IconButton
variant='outlined'
size='sm'
color='neutral'
sx={{
height: 24,
borderRadius: 24,
minWidth: 100,
// minWidth: 100,
}}
startDecorator={<EditCalendar />}
onClick={() => {
setActiveTextField('task')
setTaskInputFocus(taskInputFocus + 1)
}}
>
Task
</Button>
<EditCalendar />
</IconButton>
)}
{activeTextField != 'search' && (
<Button
<IconButton
variant='outlined'
color='neutral'
size='sm'
@ -417,7 +416,6 @@ const MyChores = () => {
height: 24,
borderRadius: 24,
}}
startDecorator={<Search />}
onClick={() => {
setActiveTextField('search')
setSearchInputFocus(searchInputFocus + 1)
@ -429,119 +427,11 @@ const MyChores = () => {
searchInputRef.current.value?.length
}}
>
Search
</Button>
<Search />
</IconButton>
)}
<Divider orientation='vertical' />
<IconButtonWithMenu
icon={<PriorityHigh />}
title='Filter by Priority'
options={Priorities}
selectedItem={selectedFilter}
onItemSelect={selected => {
handleLabelFiltering({ priority: selected.value })
}}
mouseClickHandler={handleMenuOutsideClick}
isActive={selectedFilter.startsWith('Priority: ')}
/>
<IconButtonWithMenu
icon={<Style />}
// TODO : this need simplification we want to display both user labels and chore labels
// 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}
onItemSelect={selected => {
handleLabelFiltering({ label: selected })
}}
isActive={selectedFilter.startsWith('Label: ')}
mouseClickHandler={handleMenuOutsideClick}
useChips
/>
<IconButton
onClick={handleFilterMenuOpen}
variant='outlined'
color={
selectedFilter &&
FILTERS[selectedFilter] &&
selectedFilter != 'All'
? 'primary'
: 'neutral'
}
size='sm'
sx={{
height: 24,
borderRadius: 24,
}}
>
<FilterAlt />
</IconButton>
<List
orientation='horizontal'
wrap
sx={{
mt: 0.2,
}}
>
<Menu
ref={menuRef}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleFilterMenuClose}
>
{Object.keys(FILTERS).map((filter, index) => (
<MenuItem
key={`filter-list-${filter}-${index}`}
onClick={() => {
const filterFunction = FILTERS[filter]
const filteredChores =
filterFunction.length === 2
? filterFunction(chores, userProfile.id)
: filterFunction(chores)
setFilteredChores(filteredChores)
setSelectedFilter(filter)
handleFilterMenuClose()
}}
>
{filter}
<Chip
color={selectedFilter === filter ? 'primary' : 'neutral'}
>
{FILTERS[filter].length === 2
? FILTERS[filter](chores, userProfile.id).length
: FILTERS[filter](chores).length}
</Chip>
</MenuItem>
))}
{selectedFilter.startsWith('Label: ') ||
(selectedFilter.startsWith('Priority: ') && (
<MenuItem
key={`filter-list-cancel-all-filters`}
onClick={() => {
setFilteredChores(chores)
setSelectedFilter('All')
}}
>
Cancel All Filters
</MenuItem>
))}
</Menu>
</List>
<Divider orientation='vertical' />
<IconButtonWithMenu
title='Group by'
icon={<Sort />}
@ -561,6 +451,113 @@ const MyChores = () => {
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 />}
options={Priorities}
selectedItem={selectedFilter}
onItemSelect={selected => {
handleLabelFiltering({ priority: selected.value })
}}
mouseClickHandler={handleMenuOutsideClick}
isActive={selectedFilter.startsWith('Priority: ')}
/>
<IconButtonWithMenu
key={'icon-menu-labels-filter'}
label={' Labels'}
icon={<Style />}
options={userLabels}
selectedItem={selectedFilter}
onItemSelect={selected => {
handleLabelFiltering({ label: selected })
}}
isActive={selectedFilter.startsWith('Label: ')}
mouseClickHandler={handleMenuOutsideClick}
useChips
/>
<Button
onClick={handleFilterMenuOpen}
variant='outlined'
startDecorator={<FilterAlt />}
color={
selectedFilter &&
FILTERS[selectedFilter] &&
selectedFilter != 'All'
? 'primary'
: 'neutral'
}
size='sm'
sx={{
height: 24,
borderRadius: 24,
}}
>
{' Other'}
</Button>
<List
orientation='horizontal'
wrap
sx={{
mt: 0.2,
}}
>
<Menu
ref={menuRef}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleFilterMenuClose}
>
{Object.keys(FILTERS).map((filter, index) => (
<MenuItem
key={`filter-list-${filter}-${index}`}
onClick={() => {
const filterFunction = FILTERS[filter]
const filteredChores =
filterFunction.length === 2
? filterFunction(chores, userProfile.id)
: filterFunction(chores)
setFilteredChores(filteredChores)
setSelectedFilter(filter)
handleFilterMenuClose()
}}
>
{filter}
<Chip
color={
selectedFilter === filter ? 'primary' : 'neutral'
}
>
{FILTERS[filter].length === 2
? FILTERS[filter](chores, userProfile.id).length
: FILTERS[filter](chores).length}
</Chip>
</MenuItem>
))}
{selectedFilter.startsWith('Label: ') ||
(selectedFilter.startsWith('Priority: ') && (
<MenuItem
key={`filter-list-cancel-all-filters`}
onClick={() => {
setFilteredChores(chores)
setSelectedFilter('All')
}}
>
Cancel All Filters
</MenuItem>
))}
</Menu>
</List>
</div>
</div>
)}
{selectedFilter !== 'All' && (
<Chip
level='title-md'

View file

@ -20,6 +20,7 @@ import { useNavigate } from 'react-router-dom'
import { CSSTransition } from 'react-transition-group'
import { UserContext } from '../../contexts/UserContext'
import { CreateChore } from '../../utils/Fetcher'
import LearnMoreButton from './LearnMore'
const VALID_DAYS = {
monday: 'Monday',
mon: 'Monday',
@ -203,7 +204,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
{
frequencyType: 'days_of_the_week',
regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)$/i,
name: 'Every {days} of the week',
name: 'Every {days}',
},
{
frequencyType: 'day_of_the_month',
@ -314,7 +315,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
const handleTextChange = e => {
if (!e.target.value) {
setTaskText('')
setOpenModal(false)
setDueDate(null)
setFrequency(null)
setFrequencyHumanReadable(null)
@ -327,6 +327,13 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
if (priority.result) setPriority(priority.result)
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(), {
forwardDate: true,
})
@ -337,13 +344,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
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) {
setOpenModal(true)
}
@ -425,7 +425,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
</Button> */}
<Typography level='h4'>Create new task</Typography>
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
Experimental
Experimental Feature
</Chip>
<Box>
<Typography level='body-sm'>Task in a sentence:</Typography>
@ -438,13 +438,54 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
placeholder='Type your full text here...'
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>
<Typography level='body-sm'>Title:</Typography>
<Input
value={taskTitle}
onChange={e => setTaskTitle(e.target.value)}
placeholder='Type your full text here...'
sx={{ width: '100%', fontSize: '16px' }}
/>
</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