diff --git a/src/App.jsx b/src/App.jsx
index 6aa369c..cbad980 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,12 +7,10 @@ import { Outlet, useNavigate } from 'react-router-dom'
import { useRegisterSW } from 'virtual:pwa-register/react'
import { registerCapacitorListeners } from './CapacitorListener'
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
-import { UserContext } from './contexts/UserContext'
import { useResource } from './queries/ResourceQueries'
import { AuthenticationProvider } from './service/AuthenticationService'
import { ErrorProvider } from './service/ErrorProvider'
-import { GetUserProfile } from './utils/Fetcher'
-import { apiManager, isTokenValid } from './utils/TokenManager'
+import { apiManager } from './utils/TokenManager'
import NetworkBanner from './views/components/NetworkBanner'
const add = className => {
document.getElementById('root').classList.add(className)
@@ -23,15 +21,14 @@ const remove = className => {
}
// TODO: Update the interval to at 60 minutes
const intervalMS = 5 * 60 * 1000 // 5 minutes
-
+const queryClient = new QueryClient({})
function App() {
const resource = useResource()
const navigate = useNavigate()
startApiManager(navigate)
startOpenReplay()
- const queryClient = new QueryClient()
+
const { mode, systemMode } = useColorScheme()
- const [userProfile, setUserProfile] = useState(null)
const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(true)
const {
@@ -72,23 +69,12 @@ function App() {
return remove('dark')
}
- const getUserProfile = () => {
- GetUserProfile()
- .then(res => {
- res.json().then(data => {
- setUserProfile(data.res)
- })
- })
- .catch(error => {})
- }
+
useEffect(() => {
setThemeClass()
}, [mode, systemMode])
useEffect(() => {
registerCapacitorListeners()
- if (isTokenValid()) {
- if (!userProfile) getUserProfile()
- }
}, [])
return (
@@ -99,10 +85,8 @@ function App() {
-
-
-
-
+
+
@@ -133,6 +117,7 @@ const startOpenReplay = () => {
const tracker = new Tracker({
projectKey: import.meta.env.VITE_OPENREPLAY_PROJECT_KEY,
})
+
tracker.start()
}
export default App
diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx
new file mode 100644
index 0000000..bb02182
--- /dev/null
+++ b/src/components/NotificationTemplate.jsx
@@ -0,0 +1,641 @@
+import { Save } from '@mui/icons-material'
+import AddIcon from '@mui/icons-material/Add'
+import DeleteIcon from '@mui/icons-material/Delete'
+import InfoIcon from '@mui/icons-material/Info'
+import NotificationsIcon from '@mui/icons-material/Notifications'
+import Alert from '@mui/joy/Alert'
+import Badge from '@mui/joy/Badge'
+import Box from '@mui/joy/Box'
+import Button from '@mui/joy/Button'
+import IconButton from '@mui/joy/IconButton'
+import Input from '@mui/joy/Input'
+import Option from '@mui/joy/Option'
+import Select from '@mui/joy/Select'
+import Typography from '@mui/joy/Typography'
+import { useCallback, useEffect, useState } from 'react'
+
+const timeUnits = [
+ { label: 'Mins', value: 'minutes' },
+ { label: 'Hours', value: 'hours' },
+ { label: 'Days', value: 'days' },
+]
+
+const beforeAfterOptions = [
+ { label: 'Before', value: 'before' },
+ { label: 'On Due', value: 'ondue' },
+ { label: 'After', value: 'after' },
+]
+
+function getRelativeLabel(notification) {
+ const { value, unit, type } = notification
+ if (type === 'ondue') {
+ return 'On due date'
+ }
+ return `${value} ${unit} ${type === 'before' ? 'before' : 'after'} due`
+}
+
+const NotificationTemplate = ({
+ maxNotifications = 5,
+ onChange,
+ value,
+ showTimeline = true,
+}) => {
+ const [templateName, setTemplateName] = useState(
+ value?.name || 'New Notification Template',
+ )
+ const [notifications, setNotifications] = useState(
+ value?.templates ||
+ JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
+ [],
+ )
+
+ const [error, setError] = useState(null)
+ const [showSaveDefault, setShowSaveDefault] = useState(false)
+ // Create a map of notification indices for timeline display
+ const [notificationIndexMap, setNotificationIndexMap] = useState({})
+
+ const updateNotificationIndices = useCallback(() => {
+ // Sort notifications for consistent ordering
+ const sorted = [...notifications].sort((a, b) => {
+ // Always ensure correct ordering: Before Due -> On Due -> After Due
+ if (a.type !== b.type) {
+ // Before Due first
+ if (a.type === 'before') return -1
+ if (b.type === 'before') return 1
+
+ // On Due comes before After Due
+ if (a.type === 'ondue') return -1
+ if (b.type === 'ondue') return 1
+ // DEFAULT CASE ( NOT SURE FOR FUTURE )?
+ return 0
+ }
+
+ const getMinutes = notif => {
+ const { value, unit } = notif
+ let minutes = value
+ if (unit === 'hours') minutes *= 60
+ if (unit === 'days') minutes *= 24 * 60
+ return minutes
+ }
+
+ // For Before Due: sort in descending order (furthest from due first)
+ // For After Due: sort in ascending order (closest to due first)
+ const aMinutes = getMinutes(a)
+ const bMinutes = getMinutes(b)
+ return a.type === 'before' ? bMinutes - aMinutes : aMinutes - bMinutes
+ })
+
+ const indexMap = {}
+ sorted.forEach((item, index) => {
+ const originalIdx = notifications.findIndex(
+ n =>
+ n.value === item.value &&
+ n.unit === item.unit &&
+ n.type === item.type,
+ )
+ indexMap[originalIdx] = index + 1
+ })
+
+ setNotificationIndexMap(indexMap)
+ }, [notifications])
+
+ // Sort notifications and update the index mapping
+ useEffect(() => {
+ updateNotificationIndices()
+ setError(null)
+ }, [updateNotificationIndices])
+
+ // Notify parent component of changes including the template name
+ useEffect(() => {
+ if (onChange) {
+ onChange({ notifications })
+ }
+ }, [notifications, onChange])
+
+ // Validates if a notification configuration already exists
+ const isDuplicate = (notification, currentIdx = -1) => {
+ return notifications.some((n, idx) => {
+ if (idx === currentIdx) return false
+
+ return (
+ n.value === notification.value &&
+ n.unit === notification.unit &&
+ n.type === notification.type
+ )
+ })
+ }
+
+ const handleChange = (idx, field, value) => {
+ let updatedNotification = {
+ ...notifications[idx],
+ [field]: value,
+ }
+
+ // Special handling for "On Due" option
+ if (field === 'type' && value === 'ondue') {
+ // Set default values for On Due (not applicable)
+ updatedNotification = {
+ ...updatedNotification,
+ value: 1,
+ unit: 'minutes',
+ }
+
+ // Check if another notification is already "On Due"
+ const existingOnDue = notifications.findIndex(
+ (n, i) => i !== idx && n.type === 'ondue',
+ )
+
+ if (existingOnDue !== -1) {
+ setError(
+ 'Only one notification can be set to "On Due". Please choose a different timing.',
+ )
+ return
+ }
+ }
+
+ if (isDuplicate(updatedNotification, idx)) {
+ setError(
+ 'This notification setting already exists. Please use a different timing.',
+ )
+ return
+ }
+
+ const updated = notifications.map((n, i) =>
+ i === idx ? updatedNotification : n,
+ )
+ setNotifications(updated)
+ setError(null)
+ }
+
+ const addSmartNotification = type => {
+ if (notifications.length >= maxNotifications) return
+ setShowSaveDefault(true)
+ let newNotification
+ let suggestions = []
+
+ switch (type) {
+ case 'reminder':
+ // Suggest common reminder times that don't exist
+ suggestions = [
+ { value: 1, unit: 'hours', type: 'before' },
+ { value: 1, unit: 'days', type: 'before' },
+ { value: 30, unit: 'minutes', type: 'before' },
+ { value: 2, unit: 'hours', type: 'before' },
+ { value: 3, unit: 'days', type: 'before' },
+ ]
+ break
+
+ case 'due':
+ if (notifications.some(n => n.type === 'ondue')) {
+ setError('Only one "Due Alert" notification is allowed.')
+ return
+ }
+ newNotification = { value: 0, unit: 'minutes', type: 'ondue' }
+ break
+
+ case 'followup':
+ suggestions = [
+ { value: 1, unit: 'hours', type: 'after' },
+ { value: 1, unit: 'days', type: 'after' },
+ { value: 3, unit: 'days', type: 'after' },
+ { value: 1, unit: 'weeks', type: 'after' },
+ ]
+ break
+ }
+
+ // For reminder/followup, find first non-duplicate suggestion
+ if (suggestions.length > 0) {
+ newNotification = suggestions.find(suggestion => !isDuplicate(suggestion))
+
+ if (!newNotification) {
+ setError(`All common ${type} times are already configured.`)
+ return
+ }
+ }
+
+ // Insert the new notification in the correct chronological position
+ const updatedNotifications = [...notifications, newNotification].sort(
+ (a, b) => {
+ // Convert everything to minutes for consistent comparison
+ const getMinutes = notif => {
+ const { value, unit, type } = notif
+ // On Due is exactly at due date (0 minutes)
+ if (type === 'ondue') return 0
+
+ let minutes = value
+ if (unit === 'hours') minutes *= 60
+ if (unit === 'days') minutes *= 24 * 60
+ return type === 'before' ? -minutes : minutes
+ }
+
+ return getMinutes(a) - getMinutes(b)
+ },
+ )
+
+ setNotifications(updatedNotifications)
+ setError(null)
+ }
+
+ const removeNotification = idx => {
+ const updated = notifications.filter((_, i) => i !== idx)
+ setNotifications(updated)
+ onChange && onChange(updated)
+ }
+ const renderTimeline = () => {
+ // Sort notifications chronologically
+ const sorted = [...notifications].sort((a, b) => {
+ // Convert everything to minutes for consistent comparison
+ const getMinutes = notif => {
+ const { value, unit, type } = notif
+ // On Due is exactly at due date (0 minutes)
+ if (type === 'ondue') return 0
+
+ let minutes = value
+ if (unit === 'hours') minutes *= 60
+ if (unit === 'days') minutes *= 24 * 60
+ return type === 'before' ? -minutes : minutes
+ }
+
+ return getMinutes(a) - getMinutes(b)
+ })
+
+ // Create a map to track sorted indices for original notifications
+ const notificationIndexMap = {}
+ sorted.forEach((item, index) => {
+ // Find the original index of this item in notifications array
+ const originalIdx = notifications.findIndex(
+ n =>
+ n.value === item.value &&
+ n.unit === item.unit &&
+ n.type === item.type,
+ )
+ notificationIndexMap[originalIdx] = index + 1
+ }) // Get min and max notification times for dynamic scaling
+ const minutesValues = sorted.map(n => {
+ if (n.type === 'ondue') return 0
+
+ let minutes = n.value
+ if (n.unit === 'hours') minutes *= 60
+ if (n.unit === 'days') minutes *= 24 * 60
+ return n.type === 'before' ? -minutes : minutes
+ })
+
+ const minBefore = Math.min(0, ...minutesValues) // Default to 0 if no "before" notifications
+ const maxAfter = Math.max(0, ...minutesValues) // Default to 0 if no "after" notifications
+
+ const getPositionPercent = minutes => {
+ // Due date is always at center (50%)
+ if (minutes === 0) return 50
+
+ // For notifications before due date
+ if (minutes < 0) {
+ if (minBefore === 0) return 30 // Default position if no before notifications
+ // Scale between 10% (furthest left) and 45% (closest to due)
+ return 45 - (Math.abs(minutes) / Math.abs(minBefore)) * 35
+ }
+
+ // For notifications after due date
+ if (maxAfter === 0) return 70 // Default position if no after notifications
+ // Scale between 55% (closest to due) and 90% (furthest right)
+ return 55 + (minutes / maxAfter) * 35
+ }
+
+ return (
+
+
+ Notification Timeline
+
+
+ {/* Timeline line */}
+
+ {/* Due date marker */}
+
+
+ Due Date
+
+
+
+ {/* Notification markers */}
+ {sorted.map((n, i) => {
+ // Convert to minutes for consistent scale
+ let minutes = 0
+ if (n.type !== 'ondue') {
+ minutes = n.value
+ if (n.unit === 'hours') minutes *= 60
+ if (n.unit === 'days') minutes *= 24 * 60
+ if (n.type === 'before') minutes = -minutes
+ }
+ // On Due notifications are always at the due date (0 minutes)
+
+ // Calculate position based on dynamic scaling
+ const percent = getPositionPercent(minutes)
+
+ return (
+
+
+
+
+
+ )
+ })}
+
+
+
+ )
+ }
+
+ return (
+
+ {/*
+ Schedule Name
+ */}
+
+ {/* Template Name Field */}
+ {/*
+
+ Template Name
+
+
+ */}
+
+ {error && (
+ }
+ >
+ {error}
+
+ )}
+
+ {notifications.map((n, idx) => {
+ // Get ordered badge number from timeline sorting
+ const badgeNumber = notificationIndexMap[idx]
+
+ return (
+
+
+
+ {/* Empty box to attach badge to */}
+
+
+
+ {/* Show disabled fields for "On Due" option for visual consistency */}
+
+ handleChange(idx, 'value', Math.max(1, Number(e.target.value)))
+ }
+ sx={{
+ width: 70,
+ mr: 1,
+ opacity: n.type === 'ondue' ? 0.6 : 1,
+ ...(n.type === 'ondue' && {
+ '& input': {
+ textAlign: 'center',
+ },
+ }),
+ }}
+ size={'sm'}
+ placeholder={n.type === 'ondue' ? '—' : ''}
+ />
+
+ removeNotification(idx)}
+ disabled={notifications.length === 1}
+ color={'danger'}
+ size={'sm'}
+ sx={{ mr: 1 }}
+ variant={'soft'}
+ >
+
+
+
+ )
+ })}
+
+
+
+
+
+ {showSaveDefault && (
+ }
+ onClick={() => {
+ localStorage.setItem(
+ 'defaultNotificationTemplate',
+ JSON.stringify(notifications),
+ )
+ setShowSaveDefault(false)
+ }}
+ >
+ Save as Default for Future Tasks
+
+ )}
+
+ {showTimeline && renderTimeline()}
+
+ )
+}
+
+export default NotificationTemplate
diff --git a/src/contexts/UserContext.js b/src/contexts/UserContext.js
deleted file mode 100644
index 7d2527e..0000000
--- a/src/contexts/UserContext.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createContext } from 'react'
-
-const UserContext = createContext({
- userProfile: null,
- setUserProfile: () => {},
-})
-
-export { UserContext }
diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx
index a97db03..4c722ea 100644
--- a/src/queries/UserQueries.jsx
+++ b/src/queries/UserQueries.jsx
@@ -1,5 +1,10 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
-import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
+import {
+ GetAllCircleMembers,
+ GetAllUsers,
+ GetUserProfile,
+} from '../utils/Fetcher'
+import { isTokenValid } from '../utils/TokenManager'
export const useAllUsers = () => {
return useQuery({
@@ -22,3 +27,27 @@ export const useCircleMembers = () => {
return { data, error, isLoading, handleRefetch }
}
+
+export const useUserProfile = () => {
+ const queryClient = useQueryClient()
+
+ const { data, error, isLoading } = useQuery({
+ queryKey: ['userProfile'],
+ queryFn: async () => {
+ const resp = await GetUserProfile()
+ const result = await resp.json()
+ if (!isTokenValid()) {
+ return null // Token is invalid, return null to indicate no profile
+ }
+ return result.res // Return the actual user profile data
+ },
+ staleTime: 30 * 60 * 1000, // 30 minutes in milliseconds
+ gcTime: 30 * 60 * 1000, // 30 minutes in milliseconds
+ })
+ return {
+ data,
+ error,
+ isLoading,
+ refetch: () => queryClient.invalidateQueries(['userProfile']),
+ }
+}
diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx
index eb10a51..bb54fdb 100644
--- a/src/utils/TokenManager.jsx
+++ b/src/utils/TokenManager.jsx
@@ -68,12 +68,16 @@ export async function UploadFile(url, options) {
export async function Fetch(url, options) {
if (!isTokenValid()) {
Cookies.set('ca_redirect', window.location.pathname)
- window.location.href = '/login'
+ if (!window.location.pathname === '/login') {
+ window.location.href = '/login'
+ }
}
if (!options) {
options = {}
}
+ // clone options to avoid mutation
+ const cacheKey = { ...options }
options.headers = { ...options.headers, ...HEADERS() }
const baseURL = apiManager.getApiURL()
diff --git a/src/views/Authorization/Authenticating.jsx b/src/views/Authorization/Authenticating.jsx
index 0003fdd..b90215c 100644
--- a/src/views/Authorization/Authenticating.jsx
+++ b/src/views/Authorization/Authenticating.jsx
@@ -1,16 +1,16 @@
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
-import { useContext, useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
import Logo from '../../Logo'
import { apiManager } from '../../utils/TokenManager'
import Cookies from 'js-cookie'
import { useRef } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
-import { UserContext } from '../../contexts/UserContext'
+import { useUserProfile } from '../../queries/UserQueries'
import { GetUserProfile } from '../../utils/Fetcher'
const AuthenticationLoading = () => {
- const { userProfile, setUserProfile } = useContext(UserContext)
+ const { data: userProfile, refetch: refetchUserProfile } = useUserProfile()
const Navigate = useNavigate()
const hasCalledHandleOAuth2 = useRef(false)
const [message, setMessage] = useState('Authenticating')
@@ -29,15 +29,16 @@ const AuthenticationLoading = () => {
const getUserProfileAndNavigateToHome = () => {
GetUserProfile().then(data => {
data.json().then(data => {
- setUserProfile(data.res)
- // check if redirect url is set in cookie:
- const redirectUrl = Cookies.get('ca_redirect')
- if (redirectUrl) {
- Cookies.remove('ca_redirect')
- Navigate(redirectUrl)
- } else {
- Navigate('/my/chores')
- }
+ refetchUserProfile.then(() => {
+ // check if redirect url is set in cookie:
+ const redirectUrl = Cookies.get('ca_redirect')
+ if (redirectUrl) {
+ Cookies.remove('ca_redirect')
+ Navigate(redirectUrl)
+ } else {
+ Navigate('/my/chores')
+ }
+ })
})
})
}
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index aba8e6d..11261ef 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -16,24 +16,26 @@ import {
Typography,
} from '@mui/joy'
import Cookies from 'js-cookie'
-import React, { useEffect } from 'react'
+import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { LoginSocialGoogle } from 'reactjs-social-login'
import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
-import { UserContext } from '../../contexts/UserContext'
import Logo from '../../Logo'
import { useResource } from '../../queries/ResourceQueries'
-import { GetUserProfile, login } from '../../utils/Fetcher'
+import { useUserProfile } from '../../queries/UserQueries'
+import { login } from '../../utils/Fetcher'
import { apiManager } from '../../utils/TokenManager'
import MFAVerificationModal from './MFAVerificationModal'
const LoginView = () => {
- const { userProfile, setUserProfile } = React.useContext(UserContext)
- const [username, setUsername] = React.useState('')
- const [password, setPassword] = React.useState('')
- const [error, setError] = React.useState(null)
- const [mfaModalOpen, setMfaModalOpen] = React.useState(false)
- const [mfaSessionToken, setMfaSessionToken] = React.useState('')
+ // Only fetch user profile if token is valid to prevent unnecessary queries
+ const { data: userProfileData } = useUserProfile()
+ const [userProfile, setUserProfile] = useState(null)
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [error, setError] = useState(null)
+ const [mfaModalOpen, setMfaModalOpen] = useState(false)
+ const [mfaSessionToken, setMfaSessionToken] = useState('')
const { data: resource } = useResource()
const Navigate = useNavigate()
useEffect(() => {
@@ -136,19 +138,17 @@ const LoginView = () => {
})
}
const getUserProfileAndNavigateToHome = () => {
- GetUserProfile().then(data => {
- data.json().then(data => {
- setUserProfile(data.res)
- // check if redirect url is set in cookie:
- const redirectUrl = Cookies.get('ca_redirect')
- if (redirectUrl) {
- Cookies.remove('ca_redirect')
- Navigate(redirectUrl)
- } else {
- Navigate('/my/chores')
- }
- })
- })
+ // Refetch user profile after login
+ // refetchUserProfile().then(() => {
+ // // check if redirect url is set in cookie:
+ const redirectUrl = Cookies.get('ca_redirect')
+ if (redirectUrl) {
+ Cookies.remove('ca_redirect')
+ Navigate(redirectUrl)
+ } else {
+ Navigate('/my/chores')
+ }
+ // })
}
const handleMFASuccess = data => {
@@ -274,7 +274,6 @@ const LoginView = () => {
type='submit'
fullWidth
size='lg'
- q
variant='plain'
sx={{
width: '100%',
@@ -283,7 +282,6 @@ const LoginView = () => {
borderRadius: '8px',
}}
onClick={() => {
- setUserProfile(null)
localStorage.removeItem('ca_token')
localStorage.removeItem('ca_expiration')
// go to login page:
@@ -353,7 +351,6 @@ const LoginView = () => {
type='submit'
fullWidth
size='lg'
- q
variant='plain'
sx={{
width: '100%',
@@ -382,7 +379,7 @@ const LoginView = () => {
onResolve={({ provider, data }) => {
loggedWithProvider(provider, data)
}}
- onReject={err => {
+ onReject={() => {
setError("Couldn't log in with Google, please try again")
}}
>
diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx
index 27dd9a7..e2c8bc0 100644
--- a/src/views/ChoreEdit/ChoreEdit.jsx
+++ b/src/views/ChoreEdit/ChoreEdit.jsx
@@ -24,14 +24,15 @@ import {
Typography,
} from '@mui/joy'
import moment from 'moment'
-import { useContext, useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
-import { UserContext } from '../../contexts/UserContext'
+import NotificationTemplate from '../../components/NotificationTemplate.jsx'
import {
useChore,
useCreateChore,
useUpdateChore,
} from '../../queries/ChoreQueries.jsx'
+import { useUserProfile } from '../../queries/UserQueries.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
DeleteChore,
@@ -61,7 +62,8 @@ const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once']
const NO_DUE_DATE_ALLOWED_TYPE = ['trigger']
const ChoreEdit = () => {
- const { userProfile, setUserProfile } = useContext(UserContext)
+ const { data: userProfile } = useUserProfile()
+
const [chore, setChore] = useState([])
const [choresHistory, setChoresHistory] = useState([])
const [userHistory, setUserHistory] = useState({})
@@ -708,6 +710,10 @@ const ChoreEdit = () => {
{
setIsNotificable(e.target.checked)
+ // if unchecking, reset notification metadata:
+ if (!e.target.checked) {
+ setNotificationMetadata({})
+ }
}}
defaultChecked={isNotificable}
checked={isNotificable}
@@ -726,7 +732,6 @@ const ChoreEdit = () => {
{isNotificable && (
{
}}
>
-
- What things should trigger the notification?
-
- {[
- {
- title: 'Due Date/Time',
- description: 'A simple reminder that a task is due',
- id: 'dueDate',
- },
- // {
- // title: 'Upon Completion',
- // description: 'A notification when a task is completed',
- // id: 'completion',
- // },
- {
- title: 'Predued',
- description: 'before a task is due in few hours',
- id: 'predue',
- },
- // {
- // title: 'Overdue',
- // description: 'A notification when a task is overdue',
- // id: 'overdue',
- // },
- {
- title: 'Nagging',
- description: 'Daily reminders until the task is completed',
- id: 'nagging',
- },
- ].map(item => (
-
- {
- setNotificationMetadata({
- ...notificationMetadata,
- [item.id]: !notificationMetadata[item.id],
- })
- }}
- checked={
- notificationMetadata ? notificationMetadata[item.id] : false
+ Notification Schedule:
+
+ {
+ const newNotificaitonMetadata = {
+ ...notificationMetadata,
+ templates: metadata.notifications,
}
- label={item.title}
- key={item.title}
- />
- {item.description}
-
- ))}
-
-
- What things should trigger the notification?
-
+ setNotificationMetadata(newNotificaitonMetadata)
+ }}
+ value={notificationMetadata}
+ />
+
+ Choose Who to Notify:
{
onChange={(event, newValue) => {
setLabelsV2(userLabels.filter(l => newValue.indexOf(l.name) > -1))
}}
- value={labelsV2.map(l => l.name)}
+ value={labelsV2?.map(l => l.name)}
renderValue={selected => (
{labelsV2.map(selectedOption => {
diff --git a/src/views/ChoreEdit/RepeatSection.jsx b/src/views/ChoreEdit/RepeatSection.jsx
index 3dc16e0..f76f4be 100644
--- a/src/views/ChoreEdit/RepeatSection.jsx
+++ b/src/views/ChoreEdit/RepeatSection.jsx
@@ -17,8 +17,9 @@ import {
Typography,
} from '@mui/joy'
import moment from 'moment'
-import { useContext, useEffect, useState } from 'react'
-import { UserContext } from '../../contexts/UserContext'
+import { useEffect } from 'react'
+
+import { useUserProfile } from '../../queries/UserQueries'
import { isPlusAccount } from '../../utils/Helpers'
import ThingTriggerSection from './ThingTriggerSection'
@@ -68,7 +69,6 @@ const RepeatOnSections = ({
frequencyMetadata,
onFrequencyMetadataUpdate,
}) => {
- const [intervalUnit, setIntervalUnit] = useState('days')
// if time on frequencyMetadata is not set, try to set it to the nextDueDate if available,
// otherwise set it to 18:00 of the current day
useEffect(() => {
@@ -117,13 +117,16 @@ const RepeatOnSections = ({
onFrequencyUpdate(e.target.value)
}}
/>
- */}
-
- Frequency
-
-
+ {hasNotifications && dueDate && (
+
+ Notification Schedule
+
+ {
+ const newNotificaitonMetadata = {
+ ...notificationMetadata,
+ templates: metadata.notifications,
+ }
+ setNotificationMetadata(newNotificaitonMetadata)
+ }}
+ value={notificationMetadata}
+ showTimeline={false}
+ />
+
+
+ )}
{
}
const CalendarView = ({ chores }) => {
- const { userProfile } = React.useContext(UserContext)
+ const { data: userProfile } = useUserProfile()
+
const [selectedDate, setSeletedDate] = useState(null)
const Navigate = useNavigate()
diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js
index eb44912..718714b 100644
--- a/src/views/components/CustomParsers.js
+++ b/src/views/components/CustomParsers.js
@@ -402,3 +402,80 @@ export const parseAssignees = (inputSentence, users) => {
}
return { result: null, cleanedSentence: sentence }
}
+
+export const parseDueDate = (inputSentence, chrono) => {
+ // Parse the due date using chrono
+ const parsedDueDate = chrono.parse(inputSentence, new Date(), {
+ forwardDate: true,
+ })
+
+ if (!parsedDueDate[0] || parsedDueDate[0].index === -1) {
+ return {
+ result: null,
+ highlight: [],
+ cleanedSentence: inputSentence,
+ }
+ }
+
+ const dueDateMatch = parsedDueDate[0]
+ const dueDateText = dueDateMatch.text
+ const dueDateStartIndex = dueDateMatch.index
+ const dueDateEndIndex = dueDateStartIndex + dueDateText.length
+
+ // Define words that might precede the due date and should be removed
+ const precedingWords = [
+ 'starting',
+ 'from',
+ 'beginning',
+ 'begin',
+ 'commence',
+ 'commencing',
+ ]
+
+ // Look for preceding words before the due date
+ let cleanStartIndex = dueDateStartIndex
+ let highlightStartIndex = dueDateStartIndex
+ let precedingWord = ''
+
+ // Extract text before the due date to check for preceding words
+ const textBeforeDueDate = inputSentence.substring(0, dueDateStartIndex).trim()
+
+ for (const word of precedingWords) {
+ // Check if the text before due date ends with this preceding word
+ const wordPattern = new RegExp(`\\b${word}\\s*$`, 'i')
+ const match = textBeforeDueDate.match(wordPattern)
+
+ if (match) {
+ // Found a preceding word, include it in the text to be removed
+ const matchStart = textBeforeDueDate.length - match[0].length
+ cleanStartIndex = matchStart
+ highlightStartIndex = matchStart
+ precedingWord = match[0].trim()
+ break
+ }
+ }
+
+ // Create the highlight text
+ const fullHighlightText = precedingWord
+ ? `${precedingWord} ${dueDateText}`
+ : dueDateText
+
+ // Create cleaned sentence by removing the full match (preceding word + due date)
+ const textToRemove = inputSentence.substring(cleanStartIndex, dueDateEndIndex)
+ const cleanedSentence = inputSentence
+ .replace(textToRemove, '')
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
+ .trim()
+
+ return {
+ result: dueDateMatch.start.date(),
+ highlight: [
+ {
+ text: fullHighlightText,
+ start: highlightStartIndex,
+ end: dueDateEndIndex,
+ },
+ ],
+ cleanedSentence: cleanedSentence,
+ }
+}