diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index c23f2f4..8aee228 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -692,10 +692,16 @@ const ChoreEdit = () => { Get Reminders when this task is due or completed {!isPlusAccount(userProfile) && ( - Not available in Basic Plan + Plus Feature )} + {!isPlusAccount(userProfile) && ( + + Task notifications are not available in the Basic plan. Upgrade to + Plus to receive reminders when tasks are due or completed. + + )} { 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 = ( @@ -339,8 +337,7 @@ const RepeatSection = ({ isAttemptToSave, selectedThing, }) => { - const [repeatOn, setRepeatOn] = useState('interval') - const { userProfile, setUserProfile } = useContext(UserContext) + const { userProfile } = useContext(UserContext) return ( Repeat : @@ -375,7 +372,7 @@ const RepeatSection = ({ '--ListItem-radius': '20px', }} > - {FREQUENCY_TYPES_RADIOS.map((item, index) => ( + {FREQUENCY_TYPES_RADIOS.map(item => ( - Not available in Basic Plan + Plus Feature )} + {!isPlusAccount(userProfile) && ( + + Thing-based triggers are not available in the Basic plan. Upgrade to + Plus to automatically trigger tasks when device states change. + + )} {frequencyType === 'trigger' && ( { 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: , } + } else if (activity.status === 2) { + // skipped + return { + color: 'warning', + text: 'Skipped', + icon: , + } } - 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: , } } else { return { - color: 'warning', + color: 'primary', text: 'Late', icon: , } @@ -97,13 +105,14 @@ const ActivityItem = ({ activity, members }) => { {activity.choreName} - {getTimeDisplay(activity.completedAt)} + {getTimeDisplay(activity.performedAt)} {/* Who completed it */} {/* Status chip */} + { 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' }) => { }} > - {title} { 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' }) => { }} > - + {title} { }} > - + {title} diff --git a/src/views/Chores/CompactCard.jsx b/src/views/Chores/CompactCard.jsx deleted file mode 100644 index 39ba804..0000000 --- a/src/views/Chores/CompactCard.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, Checkbox, Typography } from '@mui/joy' - -export const CompactCard = ({ - chore, - performers, - onChoreUpdate, - onChoreRemove, - userLabels, - sx, - viewOnly, - onChipClick, -}) => { - return ( - - handleToggle(task.id)} - /> - - {chore.name} - - - ) -} diff --git a/src/views/Chores/CompactChoreCard.jsx b/src/views/Chores/CompactChoreCard.jsx new file mode 100644 index 0000000..ed4a4ec --- /dev/null +++ b/src/views/Chores/CompactChoreCard.jsx @@ -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 + } else if (chore.frequencyType === 'trigger') { + return + } else { + return + } + } + + 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 ( + + navigate(`/chores/${chore.id}`)} + > + {/* Left side - Content */} + + + {/* Line 1: Name + Due Date + Frequency */} + + + {/* Chore Name */} + + {chore.name} + + + + {/* Due Date */} + + {getDueDateText(chore.nextDueDate)} + + + + {/* Line 2: Metadata */} + + {getFrequencyIcon(chore)} + + {formatMetadata()} + + + {/* Labels */} + {chore.priority > 0 && ( + 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} + + )} + {chore.labelsV2?.map(l => ( +
{ + 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}`} + > + + {l?.name} + +
+ ))} +
+
+ + {/* Right side - Actions */} + + {/* Complete Button */} + { + e.stopPropagation() + handleTaskCompletion() + }} + disabled={isPendingCompletion || notInCompletionWindow(chore)} + sx={{ + width: 32, + height: 32, + borderRadius: '50%', + }} + > + {isPendingCompletion ? ( + + ) : ( + + )} + + + {/* Menu Button */} + { + e.stopPropagation() + handleMenuOpen(e) + }} + sx={{ + width: 28, + height: 28, + opacity: 0.6, + '&:hover': { + opacity: 1, + }, + }} + > + + + + + {/* Menu */} + + setIsCompleteWithNoteModalOpen(true)}> + + Complete with note + + setIsCompleteWithPastDateModalOpen(true)}> + + Complete in past + + { + 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, + }) + } + }) + }} + > + + Skip to next due date + + setIsChangeAssigneeModalOpen(true)}> + + Delegate to someone else + + + navigate(`/chores/${chore.id}/history`)}> + + History + + + setIsChangeDueDateModalOpen(true)}> + + Change due date + + setIsNFCModalOpen(true)}> + + Write to NFC + + + + Edit + + + + Clone + + + + View + + + {chore.isActive ? : } + {chore.isActive ? 'Archive' : 'Unarchive'} + + + + + Delete + + +
+ + {/* All modals (same as original) */} + setIsChangeDueDateModalOpen(false)} + onSave={handleChangeDueDate} + /> + + setIsCompleteWithPastDateModalOpen(false)} + onSave={handleCompleteWithPastDate} + /> + + setIsChangeAssigneeModalOpen(false)} + onSave={selected => handleAssigneChange(selected.id)} + /> + + {confirmModelConfig?.isOpen && ( + + )} + + setIsCompleteWithNoteModalOpen(false)} + okText={'Complete'} + onSave={handleCompleteWithNote} + /> + + setIsNFCModalOpen(false), + }} + /> + + {/* Snackbar for pending completion */} + { + if (timeoutId) { + clearTimeout(timeoutId) + setIsPendingCompletion(false) + setTimeoutId(null) + setSecondsLeftToCancel(null) + } + }} + size='sm' + variant='outlined' + color='primary' + startDecorator={} + > + Cancel + + } + > + + Task will be marked as completed in {secondsLeftToCancel} seconds + + +
+ ) +} + +export default CompactChoreCard diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 06d9b6a..d095e8f 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -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 ( + + ) + } + const updateChores = newChore => { const newChores = chores newChores.push(newChore) @@ -486,6 +514,24 @@ const MyChores = () => { }} mouseClickHandler={handleMenuOutsideClick} /> + + {/* View Mode Toggle Button */} + + {isCompactView ? : } +
{showSearchFilter && (
@@ -626,56 +672,46 @@ const MyChores = () => { Current Filter: {searchFilter} )} - {filteredChores.length === 0 && - archivedChores==null && - ( - - - - Nothing scheduled - - {chores.length > 0 && ( - <> - - - )} - , - )} - {(searchTerm?.length > 0 || searchFilter !== 'All') && - filteredChores.map(chore => ( - + - ))} + + Nothing scheduled + + {chores.length > 0 && ( + <> + + + )} + + )} + {(searchTerm?.length > 0 || searchFilter !== 'All') && + filteredChores.map(chore => + renderChoreCard(chore, `filtered-${chore.id}`), + )} {searchTerm.length === 0 && searchFilter === 'All' && ( {choreSections.map((section, index) => { @@ -735,17 +771,7 @@ const MyChores = () => { my: 0, }} > - {section.content?.map(chore => ( - - ))} + {section.content?.map(chore => renderChoreCard(chore))} ) @@ -797,17 +823,7 @@ const MyChores = () => { - {archivedChores?.map(chore => ( - - ))} + {archivedChores?.map(chore => renderChoreCard(chore))} )} diff --git a/src/views/Settings/APITokenSettings.jsx b/src/views/Settings/APITokenSettings.jsx index b896688..2d71b42 100644 --- a/src/views/Settings/APITokenSettings.jsx +++ b/src/views/Settings/APITokenSettings.jsx @@ -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 {!isPlusAccount(userProfile) && ( - - Not available in Basic Plan - + <> + + Plus Feature + + + 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. + + )} {tokens.map(token => ( diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx index 58c98f2..42f7af3 100644 --- a/src/views/Settings/Settings.jsx +++ b/src/views/Settings/Settings.jsx @@ -422,9 +422,15 @@ const Settings = () => { 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. + {!isPlusAccount(userProfile) && ( + + Webhook notifications are not available in the Basic plan. + Upgrade to Plus to receive real-time updates via webhooks. + + )} { Enable webhook notifications for tasks and things updates.{' '} {userProfile && !isPlusAccount(userProfile) && ( - Not available in Basic Plan + Plus Feature )} diff --git a/src/views/Settings/StorageSettings.jsx b/src/views/Settings/StorageSettings.jsx index 66115e1..114e6c7 100644 --- a/src/views/Settings/StorageSettings.jsx +++ b/src/views/Settings/StorageSettings.jsx @@ -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 = () => { Server Storage Usage + {!isPlusAccount(userProfile) && ( + + Plus Feature + + )} This is the storage used by your account on our servers (e.g. files, images, and data you have uploaded). - {loading ? ( - Loading... + {!isPlusAccount(userProfile) ? ( + <> + + + -- MB used / -- MB total (--) + + + Server storage monitoring is not available in the Basic plan. + Upgrade to Plus to track your server storage usage. + + + ) : loading ? ( + <> + + Loading... + ) : ( <>