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 && ( + + )} + + {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) }} /> - {['hours', 'days', 'weeks', 'months', 'years'].map(item => ( */} - - 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, + } +}