Enhance user experience with Plus feature notifications and improve code structure
- Updated ChoreEdit and RepeatSection components to provide clearer messaging for Basic plan users regarding Plus features. - Introduced CompactChoreCard component to streamline chore display options in MyChores. - Implemented view mode toggle in MyChores for better user preference handling. - Refactored StorageSettings to conditionally display storage usage information based on user plan. - Improved error handling and user feedback across various components.
This commit is contained in:
@@ -692,10 +692,16 @@ const ChoreEdit = () => {
|
||||
Get Reminders when this task is due or completed
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning'>
|
||||
Not available in Basic Plan
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mb: 1 }}>
|
||||
Task notifications are not available in the Basic plan. Upgrade to
|
||||
Plus to receive reminders when tasks are due or completed.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<FormControl sx={{ mt: 1 }}>
|
||||
<Checkbox
|
||||
|
||||
@@ -65,10 +65,8 @@ const RepeatOnSections = ({
|
||||
frequencyType,
|
||||
frequency,
|
||||
onFrequencyUpdate,
|
||||
onFrequencyTypeUpdate,
|
||||
frequencyMetadata,
|
||||
onFrequencyMetadataUpdate,
|
||||
things,
|
||||
}) => {
|
||||
const [intervalUnit, setIntervalUnit] = useState('days')
|
||||
// if time on frequencyMetadata is not set, try to set it to the nextDueDate if available,
|
||||
@@ -79,7 +77,7 @@ const RepeatOnSections = ({
|
||||
moment(new Date()).format('YYYY-MM-DD') + 'T' + '18:00',
|
||||
).format()
|
||||
}
|
||||
}, [])
|
||||
}, [frequencyMetadata])
|
||||
|
||||
const timePickerComponent = (
|
||||
<Grid item sm={12} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
@@ -339,8 +337,7 @@ const RepeatSection = ({
|
||||
isAttemptToSave,
|
||||
selectedThing,
|
||||
}) => {
|
||||
const [repeatOn, setRepeatOn] = useState('interval')
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { userProfile } = useContext(UserContext)
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Typography level='h4'>Repeat :</Typography>
|
||||
@@ -375,7 +372,7 @@ const RepeatSection = ({
|
||||
'--ListItem-radius': '20px',
|
||||
}}
|
||||
>
|
||||
{FREQUENCY_TYPES_RADIOS.map((item, index) => (
|
||||
{FREQUENCY_TYPES_RADIOS.map(item => (
|
||||
<ListItem key={item}>
|
||||
<Checkbox
|
||||
// disabled={index === 0}
|
||||
@@ -557,10 +554,16 @@ const RepeatSection = ({
|
||||
Is this something that should be done when a thing state changes?{' '}
|
||||
{userProfile && !isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning'>
|
||||
Not available in Basic Plan
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</FormHelperText>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Thing-based triggers are not available in the Basic plan. Upgrade to
|
||||
Plus to automatically trigger tasks when device states change.
|
||||
</Typography>
|
||||
)}
|
||||
</FormControl>
|
||||
{frequencyType === 'trigger' && (
|
||||
<ThingTriggerSection
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
EventNote,
|
||||
Notes,
|
||||
Person,
|
||||
Redo,
|
||||
Refresh,
|
||||
Toll,
|
||||
WatchLater,
|
||||
@@ -31,9 +32,9 @@ const ActivityItem = ({ activity, members }) => {
|
||||
member => member.userId === activity.completedBy,
|
||||
)
|
||||
|
||||
const getTimeDisplay = completedAt => {
|
||||
const getTimeDisplay = performedAt => {
|
||||
const now = moment()
|
||||
const completed = moment(completedAt)
|
||||
const completed = moment(performedAt)
|
||||
const diffInHours = now.diff(completed, 'hours')
|
||||
const diffInDays = now.diff(completed, 'days')
|
||||
|
||||
@@ -49,27 +50,34 @@ const ActivityItem = ({ activity, members }) => {
|
||||
}
|
||||
|
||||
const getStatusInfo = activity => {
|
||||
if (!activity.dueDate) {
|
||||
if (!activity.status === 1) {
|
||||
return {
|
||||
color: 'neutral',
|
||||
text: 'Completed',
|
||||
icon: <CheckCircle />,
|
||||
}
|
||||
} else if (activity.status === 2) {
|
||||
// skipped
|
||||
return {
|
||||
color: 'warning',
|
||||
text: 'Skipped',
|
||||
icon: <Redo />,
|
||||
}
|
||||
}
|
||||
|
||||
const wasOnTime = moment(activity.completedAt).isSameOrBefore(
|
||||
const wasOnTime = moment(activity.performedAt).isSameOrBefore(
|
||||
moment(activity.dueDate),
|
||||
)
|
||||
|
||||
if (wasOnTime) {
|
||||
return {
|
||||
color: 'success',
|
||||
text: 'On Time',
|
||||
text: 'Done',
|
||||
icon: <CheckCircle />,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
color: 'warning',
|
||||
color: 'primary',
|
||||
text: 'Late',
|
||||
icon: <WatchLater />,
|
||||
}
|
||||
@@ -97,13 +105,14 @@ const ActivityItem = ({ activity, members }) => {
|
||||
{activity.choreName}
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='text.secondary'>
|
||||
{getTimeDisplay(activity.completedAt)}
|
||||
{getTimeDisplay(activity.performedAt)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Who completed it */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{/* Status chip */}
|
||||
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
@@ -171,7 +180,7 @@ const groupActivitiesByDate = activities => {
|
||||
const groups = {}
|
||||
|
||||
activities.forEach(activity => {
|
||||
const date = moment(activity.completedAt).format('YYYY-MM-DD')
|
||||
const date = moment(activity.performedAt).format('YYYY-MM-DD')
|
||||
if (!groups[date]) {
|
||||
groups[date] = []
|
||||
}
|
||||
@@ -229,7 +238,6 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<EventNote />
|
||||
<Typography level='title-md'>{title}</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@@ -262,7 +270,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
||||
const sortedHistory = enrichedHistory
|
||||
.sort(
|
||||
(a, b) =>
|
||||
moment(b.completedAt).valueOf() - moment(a.completedAt).valueOf(),
|
||||
moment(b.performedAt).valueOf() - moment(a.performedAt).valueOf(),
|
||||
)
|
||||
.slice(0, 10) // Show only latest 10 activities
|
||||
|
||||
@@ -286,7 +294,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<EventNote color='primary' />
|
||||
<EventNote color='' />
|
||||
<Typography level='title-md'>{title}</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@@ -331,7 +339,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<EventNote color='primary' />
|
||||
<EventNote color='' />
|
||||
<Typography level='title-md'>{title}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Box, Checkbox, Typography } from '@mui/joy'
|
||||
|
||||
export const CompactCard = ({
|
||||
chore,
|
||||
performers,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
userLabels,
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
// checked={!!task.completedAt}
|
||||
// onChange={() => handleToggle(task.id)}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
textDecoration: chore.completedAt ? 'line-through' : 'none',
|
||||
}}
|
||||
>
|
||||
{chore.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
845
src/views/Chores/CompactChoreCard.jsx
Normal file
845
src/views/Chores/CompactChoreCard.jsx
Normal file
@@ -0,0 +1,845 @@
|
||||
import {
|
||||
Archive,
|
||||
CancelScheduleSend,
|
||||
Check,
|
||||
CopyAll,
|
||||
Delete,
|
||||
Edit,
|
||||
ManageSearch,
|
||||
MoreTime,
|
||||
MoreVert,
|
||||
Nfc,
|
||||
NoteAdd,
|
||||
RecordVoiceOver,
|
||||
Repeat,
|
||||
SwitchAccessShortcut,
|
||||
TimesOneMobiledata,
|
||||
Unarchive,
|
||||
Update,
|
||||
ViewCarousel,
|
||||
Webhook,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useError } from '../../service/ErrorProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
ArchiveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
SkipChore,
|
||||
UnArchiveChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import DateModal from '../Modals/Inputs/DateModal'
|
||||
import SelectModal from '../Modals/Inputs/SelectModal'
|
||||
import TextModal from '../Modals/Inputs/TextModal'
|
||||
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
|
||||
|
||||
const CompactChoreCard = ({
|
||||
chore,
|
||||
performers,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
}) => {
|
||||
const [activeUserId, setActiveUserId] = React.useState(0)
|
||||
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
|
||||
React.useState(false)
|
||||
const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] =
|
||||
React.useState(false)
|
||||
const [isChangeAssigneeModalOpen, setIsChangeAssigneeModalOpen] =
|
||||
React.useState(false)
|
||||
const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] =
|
||||
React.useState(false)
|
||||
const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
|
||||
const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
|
||||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||
const menuRef = React.useRef(null)
|
||||
const navigate = useNavigate()
|
||||
const [isDisabled, setIsDisabled] = React.useState(false)
|
||||
|
||||
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError } = useError()
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleMenuOutsideClick)
|
||||
}
|
||||
}, [anchorEl])
|
||||
|
||||
const handleMenuOpen = event => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleMenuOutsideClick = event => {
|
||||
if (
|
||||
anchorEl &&
|
||||
!anchorEl.contains(event.target) &&
|
||||
!menuRef.current.contains(event.target)
|
||||
) {
|
||||
handleMenuClose()
|
||||
}
|
||||
}
|
||||
|
||||
// All the existing handler methods (same as original ChoreCard)
|
||||
const handleEdit = () => {
|
||||
navigate(`/chores/${chore.id}/edit`)
|
||||
}
|
||||
|
||||
const handleClone = () => {
|
||||
navigate(`/chores/${chore.id}/edit?clone=true`)
|
||||
}
|
||||
|
||||
const handleView = () => {
|
||||
navigate(`/chores/${chore.id}`)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Chore',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
message: 'Are you sure you want to delete this chore?',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
DeleteChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
onChoreRemove(chore)
|
||||
}
|
||||
})
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleArchive = () => {
|
||||
if (chore.isActive) {
|
||||
ArchiveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = { ...chore, isActive: false }
|
||||
onChoreUpdate(newChore, 'archive')
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
UnArchiveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = { ...chore, isActive: true }
|
||||
onChoreUpdate(newChore, 'unarchive')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const handleTaskCompletion = () => {
|
||||
setIsPendingCompletion(true)
|
||||
let seconds = 3
|
||||
setSecondsLeftToCancel(seconds)
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
seconds -= 1
|
||||
setSecondsLeftToCancel(seconds)
|
||||
|
||||
if (seconds <= 0) {
|
||||
clearInterval(countdownInterval)
|
||||
setIsPendingCompletion(false)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const id = setTimeout(() => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json().then(data => {
|
||||
onChoreUpdate(data.res, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Update Failed',
|
||||
message: 'Request will be reattempt when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
setTimeoutId(id)
|
||||
}
|
||||
|
||||
const handleChangeDueDate = newDate => {
|
||||
if (activeUserId === null) {
|
||||
alert('Please select a performer')
|
||||
return
|
||||
}
|
||||
UpdateDueDate(chore.id, newDate).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'rescheduled')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCompleteWithPastDate = newDate => {
|
||||
if (activeUserId === null) {
|
||||
alert('Please select a performer')
|
||||
return
|
||||
}
|
||||
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
new Date(newDate).toISOString(),
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAssigneChange = assigneeId => {
|
||||
UpdateChoreAssignee(chore.id, assigneeId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'assigned')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCompleteWithNote = note => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser
|
||||
? { note, completedBy: impersonatedUser.userId }
|
||||
: { note },
|
||||
null,
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
const getDueDateText = nextDueDate => {
|
||||
if (chore.nextDueDate === null) return 'No Due Date'
|
||||
const diff = moment(nextDueDate).diff(moment(), 'hours')
|
||||
if (diff < 24 && diff > 0) {
|
||||
return moment(nextDueDate).calendar().replace(' at', '')
|
||||
}
|
||||
if (diff < 0) {
|
||||
return 'Overdue'
|
||||
}
|
||||
return moment(nextDueDate).fromNow()
|
||||
}
|
||||
|
||||
const getDueDateColor = nextDueDate => {
|
||||
if (chore.nextDueDate === null) return 'neutral'
|
||||
const diff = moment(nextDueDate).diff(moment(), 'hours')
|
||||
if (diff < 48 && diff > 0) {
|
||||
return 'warning'
|
||||
}
|
||||
if (diff < 0) {
|
||||
return 'danger'
|
||||
}
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
const getRecurrentText = chore => {
|
||||
// if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata:
|
||||
const metadata =
|
||||
typeof chore.frequencyMetadata === 'string'
|
||||
? JSON.parse(chore.frequencyMetadata)
|
||||
: chore.frequencyMetadata
|
||||
|
||||
const dayOfMonthSuffix = n => {
|
||||
if (n >= 11 && n <= 13) {
|
||||
return 'th'
|
||||
}
|
||||
switch (n % 10) {
|
||||
case 1:
|
||||
return 'st'
|
||||
case 2:
|
||||
return 'nd'
|
||||
case 3:
|
||||
return 'rd'
|
||||
default:
|
||||
return 'th'
|
||||
}
|
||||
}
|
||||
if (chore.frequencyType === 'once') {
|
||||
return 'Once'
|
||||
} else if (chore.frequencyType === 'trigger') {
|
||||
return 'Trigger'
|
||||
} else if (chore.frequencyType === 'daily') {
|
||||
return 'Daily'
|
||||
} else if (chore.frequencyType === 'adaptive') {
|
||||
return 'Adaptive'
|
||||
} else if (chore.frequencyType === 'weekly') {
|
||||
return 'Weekly'
|
||||
} else if (chore.frequencyType === 'monthly') {
|
||||
return 'Monthly'
|
||||
} else if (chore.frequencyType === 'yearly') {
|
||||
return 'Yearly'
|
||||
} else if (chore.frequencyType === 'days_of_the_week') {
|
||||
let days = metadata.days
|
||||
if (days.length > 4) {
|
||||
const allDays = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
]
|
||||
const selectedDays = days.map(d => moment().day(d).format('dddd'))
|
||||
const notSelectedDay = allDays.filter(
|
||||
day => !selectedDays.includes(day),
|
||||
)
|
||||
const notSelectedShortdays = notSelectedDay.map(d =>
|
||||
moment().day(d).format('ddd'),
|
||||
)
|
||||
return `Daily except ${notSelectedShortdays.join(', ')}`
|
||||
} else {
|
||||
days = days.map(d => moment().day(d).format('ddd'))
|
||||
return days.join(', ')
|
||||
}
|
||||
} else if (chore.frequencyType === 'day_of_the_month') {
|
||||
let months = metadata.months
|
||||
if (months.length > 6) {
|
||||
const allMonths = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
]
|
||||
const selectedMonths = months.map(m => moment().month(m).format('MMMM'))
|
||||
const notSelectedMonth = allMonths.filter(
|
||||
month => !selectedMonths.includes(month),
|
||||
)
|
||||
const notSelectedShortMonths = notSelectedMonth.map(m =>
|
||||
moment().month(m).format('MMM'),
|
||||
)
|
||||
let result = `Monthly ${chore.frequency}${dayOfMonthSuffix(
|
||||
chore.frequency,
|
||||
)}`
|
||||
if (notSelectedShortMonths.length > 0)
|
||||
result += `
|
||||
except ${notSelectedShortMonths.join(', ')}`
|
||||
return result
|
||||
} else {
|
||||
let freqData = metadata
|
||||
const months = freqData.months.map(m => moment().month(m).format('MMM'))
|
||||
return `${chore.frequency}${dayOfMonthSuffix(
|
||||
chore.frequency,
|
||||
)} of ${months.join(', ')}`
|
||||
}
|
||||
} else if (chore.frequencyType === 'interval') {
|
||||
return `Every ${chore.frequency} ${metadata.unit}`
|
||||
} else {
|
||||
return chore.frequencyType
|
||||
}
|
||||
}
|
||||
|
||||
const getFrequencyIcon = chore => {
|
||||
if (['once', 'no_repeat'].includes(chore.frequencyType)) {
|
||||
return <TimesOneMobiledata sx={{ fontSize: 14 }} />
|
||||
} else if (chore.frequencyType === 'trigger') {
|
||||
return <Webhook sx={{ fontSize: 14 }} />
|
||||
} else {
|
||||
return <Repeat sx={{ fontSize: 14 }} />
|
||||
}
|
||||
}
|
||||
|
||||
const formatMetadata = () => {
|
||||
const parts = []
|
||||
|
||||
// Frequency
|
||||
parts.push(getRecurrentText(chore))
|
||||
|
||||
// Assignee (if not current user)
|
||||
if (userProfile && chore.assignedTo !== userProfile.id) {
|
||||
const assignee = performers.find(
|
||||
p => p.id === chore.assignedTo,
|
||||
)?.displayName
|
||||
if (assignee) parts.push(assignee)
|
||||
}
|
||||
|
||||
// Points
|
||||
if (chore.points > 0) {
|
||||
parts.push(`${chore.points}pts`)
|
||||
}
|
||||
|
||||
return parts.join(' • ')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={chore.id + '-compact-box'}>
|
||||
<Box
|
||||
style={viewOnly ? { pointerEvents: 'none' } : {}}
|
||||
sx={{
|
||||
...sx,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: 1,
|
||||
// py: 0.75,
|
||||
minHeight: 56, // More compact height
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
'&:hover': {
|
||||
bgcolor: 'background.level1',
|
||||
},
|
||||
'&:last-child': {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
}}
|
||||
onClick={() => navigate(`/chores/${chore.id}`)}
|
||||
>
|
||||
{/* Left side - Content */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
mr: 1.5,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Line 1: Name + Due Date + Frequency */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 0.25,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Chore Name */}
|
||||
<Typography
|
||||
level='title-sm'
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
{chore.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Due Date */}
|
||||
<Chip
|
||||
variant='soft'
|
||||
size='sm'
|
||||
color={getDueDateColor(chore.nextDueDate)}
|
||||
sx={{ fontSize: 10, height: 20, flexShrink: 0 }}
|
||||
>
|
||||
{getDueDateText(chore.nextDueDate)}
|
||||
</Chip>
|
||||
</Box>
|
||||
|
||||
{/* Line 2: Metadata */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
{getFrequencyIcon(chore)}
|
||||
<Typography
|
||||
level='body-xs'
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{formatMetadata()}
|
||||
</Typography>
|
||||
|
||||
{/* Labels */}
|
||||
{chore.priority > 0 && (
|
||||
<Chip
|
||||
variant='solid'
|
||||
size='sm'
|
||||
color={
|
||||
chore.priority === 1
|
||||
? 'danger'
|
||||
: chore.priority === 2
|
||||
? 'warning'
|
||||
: 'neutral'
|
||||
}
|
||||
startDecorator={
|
||||
Priorities.find(p => p.value === chore.priority)?.icon
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onChipClick({ priority: chore.priority })
|
||||
}}
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
// height: 16,
|
||||
// fontSize: 9,
|
||||
// px: 0.5,
|
||||
}}
|
||||
>
|
||||
P{chore.priority}
|
||||
</Chip>
|
||||
)}
|
||||
{chore.labelsV2?.map(l => (
|
||||
<div
|
||||
role='none'
|
||||
tabIndex={0}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onChipClick({ label: l })
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.stopPropagation()
|
||||
onChipClick({ label: l })
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
// remove any padding or margin:
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}}
|
||||
key={`compact-chorecard-${chore.id}-label-${l.id}`}
|
||||
>
|
||||
<Chip
|
||||
variant='solid'
|
||||
color='primary'
|
||||
size='sm'
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
// height: 16,
|
||||
// fontSize: 9,
|
||||
// px: 0.5,
|
||||
backgroundColor: `${l?.color} !important`,
|
||||
color: getTextColorFromBackgroundColor(l?.color),
|
||||
}}
|
||||
>
|
||||
{l?.name}
|
||||
</Chip>
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Right side - Actions */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.25,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* Complete Button */}
|
||||
<IconButton
|
||||
variant='solid'
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleTaskCompletion()
|
||||
}}
|
||||
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='sm' color='success' />
|
||||
) : (
|
||||
<Check sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
{/* Menu Button */}
|
||||
<IconButton
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleMenuOpen(e)
|
||||
}}
|
||||
sx={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
opacity: 0.6,
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MoreVert sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Menu */}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={() => setIsCompleteWithNoteModalOpen(true)}>
|
||||
<NoteAdd />
|
||||
Complete with note
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setIsCompleteWithPastDateModalOpen(true)}>
|
||||
<Update />
|
||||
Complete in past
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
SkipChore(chore.id)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'skipped')
|
||||
handleMenuClose()
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: 'Request will be processed when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SwitchAccessShortcut />
|
||||
Skip to next due date
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setIsChangeAssigneeModalOpen(true)}>
|
||||
<RecordVoiceOver />
|
||||
Delegate to someone else
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={() => navigate(`/chores/${chore.id}/history`)}>
|
||||
<ManageSearch />
|
||||
History
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={() => setIsChangeDueDateModalOpen(true)}>
|
||||
<MoreTime />
|
||||
Change due date
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setIsNFCModalOpen(true)}>
|
||||
<Nfc />
|
||||
Write to NFC
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleEdit}>
|
||||
<Edit />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClone}>
|
||||
<CopyAll />
|
||||
Clone
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleView}>
|
||||
<ViewCarousel />
|
||||
View
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleArchive} color='neutral'>
|
||||
{chore.isActive ? <Archive /> : <Unarchive />}
|
||||
{chore.isActive ? 'Archive' : 'Unarchive'}
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={handleDelete} color='danger'>
|
||||
<Delete />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
{/* All modals (same as original) */}
|
||||
<DateModal
|
||||
isOpen={isChangeDueDateModalOpen}
|
||||
key={'changeDueDate' + chore.id}
|
||||
current={chore.nextDueDate}
|
||||
title={`Change due date`}
|
||||
onClose={() => setIsChangeDueDateModalOpen(false)}
|
||||
onSave={handleChangeDueDate}
|
||||
/>
|
||||
|
||||
<DateModal
|
||||
isOpen={isCompleteWithPastDateModalOpen}
|
||||
key={'completedInPast' + chore.id}
|
||||
current={chore.nextDueDate}
|
||||
title={`Save Chore that you completed in the past`}
|
||||
onClose={() => setIsCompleteWithPastDateModalOpen(false)}
|
||||
onSave={handleCompleteWithPastDate}
|
||||
/>
|
||||
|
||||
<SelectModal
|
||||
isOpen={isChangeAssigneeModalOpen}
|
||||
options={performers}
|
||||
displayKey='displayName'
|
||||
title={`Delegate to someone else`}
|
||||
placeholder={'Select a performer'}
|
||||
onClose={() => setIsChangeAssigneeModalOpen(false)}
|
||||
onSave={selected => handleAssigneChange(selected.id)}
|
||||
/>
|
||||
|
||||
{confirmModelConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
)}
|
||||
|
||||
<TextModal
|
||||
isOpen={isCompleteWithNoteModalOpen}
|
||||
title='Add note to attach to this completion:'
|
||||
onClose={() => setIsCompleteWithNoteModalOpen(false)}
|
||||
okText={'Complete'}
|
||||
onSave={handleCompleteWithNote}
|
||||
/>
|
||||
|
||||
<WriteNFCModal
|
||||
config={{
|
||||
isOpen: isNFCModalOpen,
|
||||
url: `${window.location.origin}/chores/${chore.id}`,
|
||||
onClose: () => setIsNFCModalOpen(false),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Snackbar for pending completion */}
|
||||
<Snackbar
|
||||
open={isPendingCompletion}
|
||||
endDecorator={
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
setIsPendingCompletion(false)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
}
|
||||
}}
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
startDecorator={<CancelScheduleSend />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography level='body-xs' textAlign={'center'}>
|
||||
Task will be marked as completed in {secondsLeftToCancel} seconds
|
||||
</Typography>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default CompactChoreCard
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Sort,
|
||||
Style,
|
||||
Unarchive,
|
||||
ViewAgenda,
|
||||
ViewModule,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Accordion,
|
||||
@@ -42,6 +44,7 @@ import Priorities from '../../utils/Priorities'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import ChoreCard from './ChoreCard'
|
||||
import CompactChoreCard from './CompactChoreCard'
|
||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||
|
||||
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
||||
@@ -81,6 +84,9 @@ const MyChores = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [performers, setPerformers] = useState([])
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [isCompactView, setIsCompactView] = useState(
|
||||
localStorage.getItem('choreCardViewMode') === 'compact',
|
||||
)
|
||||
const menuRef = useRef(null)
|
||||
const Navigate = useNavigate()
|
||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||
@@ -205,6 +211,28 @@ const MyChores = () => {
|
||||
localStorage.setItem('selectedChoreFilter', value)
|
||||
}
|
||||
|
||||
const toggleViewMode = () => {
|
||||
const newMode = !isCompactView
|
||||
setIsCompactView(newMode)
|
||||
localStorage.setItem('choreCardViewMode', newMode ? 'compact' : 'default')
|
||||
}
|
||||
|
||||
// Helper function to render the appropriate card component
|
||||
const renderChoreCard = (chore, key) => {
|
||||
const CardComponent = isCompactView ? CompactChoreCard : ChoreCard
|
||||
return (
|
||||
<CardComponent
|
||||
key={key || chore.id}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const updateChores = newChore => {
|
||||
const newChores = chores
|
||||
newChores.push(newChore)
|
||||
@@ -486,6 +514,24 @@ const MyChores = () => {
|
||||
}}
|
||||
mouseClickHandler={handleMenuOutsideClick}
|
||||
/>
|
||||
|
||||
{/* View Mode Toggle Button */}
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
sx={{
|
||||
height: 32,
|
||||
width: 32,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
onClick={toggleViewMode}
|
||||
title={
|
||||
isCompactView ? 'Switch to Card View' : 'Switch to Compact View'
|
||||
}
|
||||
>
|
||||
{isCompactView ? <ViewModule /> : <ViewAgenda />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
{showSearchFilter && (
|
||||
<div className='flex gap-4'>
|
||||
@@ -626,56 +672,46 @@ const MyChores = () => {
|
||||
Current Filter: {searchFilter}
|
||||
</Chip>
|
||||
)}
|
||||
{filteredChores.length === 0 &&
|
||||
archivedChores==null &&
|
||||
(
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
height: '50vh',
|
||||
}}
|
||||
>
|
||||
<EditCalendar
|
||||
sx={{
|
||||
fontSize: '4rem',
|
||||
// color: 'text.disabled',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography level='title-md' gutterBottom>
|
||||
Nothing scheduled
|
||||
</Typography>
|
||||
{chores.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFilteredChores(chores)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
>
|
||||
Reset filters
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>,
|
||||
)}
|
||||
{(searchTerm?.length > 0 || searchFilter !== 'All') &&
|
||||
filteredChores.map(chore => (
|
||||
<ChoreCard
|
||||
key={`filtered-${chore.id} `}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
{filteredChores.length === 0 && archivedChores == null && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
height: '50vh',
|
||||
}}
|
||||
>
|
||||
<EditCalendar
|
||||
sx={{
|
||||
fontSize: '4rem',
|
||||
// color: 'text.disabled',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Typography level='title-md' gutterBottom>
|
||||
Nothing scheduled
|
||||
</Typography>
|
||||
{chores.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFilteredChores(chores)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
>
|
||||
Reset filters
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{(searchTerm?.length > 0 || searchFilter !== 'All') &&
|
||||
filteredChores.map(chore =>
|
||||
renderChoreCard(chore, `filtered-${chore.id}`),
|
||||
)}
|
||||
{searchTerm.length === 0 && searchFilter === 'All' && (
|
||||
<AccordionGroup transition='0.2s ease' disableDivider>
|
||||
{choreSections.map((section, index) => {
|
||||
@@ -735,17 +771,7 @@ const MyChores = () => {
|
||||
my: 0,
|
||||
}}
|
||||
>
|
||||
{section.content?.map(chore => (
|
||||
<ChoreCard
|
||||
key={chore.id}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
/>
|
||||
))}
|
||||
{section.content?.map(chore => renderChoreCard(chore))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
@@ -797,17 +823,7 @@ const MyChores = () => {
|
||||
</Chip>
|
||||
</Divider>
|
||||
|
||||
{archivedChores?.map(chore => (
|
||||
<ChoreCard
|
||||
key={chore.id}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
/>
|
||||
))}
|
||||
{archivedChores?.map(chore => renderChoreCard(chore))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -24,7 +24,7 @@ const APITokenSettings = () => {
|
||||
const [tokens, setTokens] = useState([])
|
||||
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
|
||||
const [showTokenId, setShowTokenId] = useState(null)
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
const { userProfile } = useContext(UserContext)
|
||||
useEffect(() => {
|
||||
GetLongLiveTokens().then(resp => {
|
||||
resp.json().then(data => {
|
||||
@@ -56,9 +56,16 @@ const APITokenSettings = () => {
|
||||
chores
|
||||
</Typography>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning'>
|
||||
Not available in Basic Plan
|
||||
</Chip>
|
||||
<>
|
||||
<Chip variant='soft' color='warning'>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
API tokens are not available in the Basic plan. Upgrade to Plus to
|
||||
generate API tokens for integrating with external systems and
|
||||
automating your tasks.
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tokens.map(token => (
|
||||
|
||||
@@ -422,9 +422,15 @@ const Settings = () => {
|
||||
</Typography>
|
||||
<Typography level='body-md' mt={-1}>
|
||||
Webhooks allow you to send real-time notifications to other
|
||||
services when events happen in your Circle. Use the webhook URL
|
||||
below to
|
||||
services when events happen in your Circle. Configure a webhook
|
||||
URL to receive real-time updates.
|
||||
</Typography>
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Webhook notifications are not available in the Basic plan.
|
||||
Upgrade to Plus to receive real-time updates via webhooks.
|
||||
</Typography>
|
||||
)}
|
||||
<FormControl sx={{ mt: 1 }}>
|
||||
<Checkbox
|
||||
checked={webhookURL !== null}
|
||||
@@ -448,7 +454,7 @@ const Settings = () => {
|
||||
Enable webhook notifications for tasks and things updates.{' '}
|
||||
{userProfile && !isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning'>
|
||||
Not available in Basic Plan
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</FormHelperText>
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Button, Card, Divider, LinearProgress, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { GetStorageUsage } from '../../utils/Fetcher'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
|
||||
const StorageSettings = () => {
|
||||
const Navigate = useNavigate()
|
||||
const { userProfile } = useContext(UserContext)
|
||||
const [usage, setUsage] = useState({ used: 0, total: 0 })
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
GetStorageUsage().then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUsage(data.res)
|
||||
setLoading(false)
|
||||
if (isPlusAccount(userProfile)) {
|
||||
GetStorageUsage().then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUsage(data.res)
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
}, [userProfile])
|
||||
|
||||
const percent =
|
||||
usage.total > 0 ? Math.round((usage.used / usage.total) * 100) : 0
|
||||
@@ -30,13 +42,42 @@ const StorageSettings = () => {
|
||||
<Card className='p-4' sx={{ maxWidth: 500, mb: 2 }}>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
Server Storage Usage
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
This is the storage used by your account on our servers (e.g. files,
|
||||
images, and data you have uploaded).
|
||||
</Typography>
|
||||
{loading ? (
|
||||
<Typography level='body-xs'>Loading...</Typography>
|
||||
{!isPlusAccount(userProfile) ? (
|
||||
<>
|
||||
<LinearProgress
|
||||
determinate
|
||||
value={0}
|
||||
sx={{
|
||||
mb: 1,
|
||||
opacity: 0.4,
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-400)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography level='body-xs' sx={{ opacity: 0.6, mb: 1 }}>
|
||||
-- MB used / -- MB total (--)
|
||||
</Typography>
|
||||
<Typography level='body-sm' color='warning'>
|
||||
Server storage monitoring is not available in the Basic plan.
|
||||
Upgrade to Plus to track your server storage usage.
|
||||
</Typography>
|
||||
</>
|
||||
) : loading ? (
|
||||
<>
|
||||
<LinearProgress sx={{ mb: 1 }} />
|
||||
<Typography level='body-xs'>Loading...</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LinearProgress determinate value={percent} sx={{ mb: 1 }} />
|
||||
|
||||
Reference in New Issue
Block a user