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
index 9970ea9..bb02182 100644
--- a/src/components/NotificationTemplate.jsx
+++ b/src/components/NotificationTemplate.jsx
@@ -1,3 +1,4 @@
+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'
@@ -14,66 +15,64 @@ import Typography from '@mui/joy/Typography'
import { useCallback, useEffect, useState } from 'react'
const timeUnits = [
- { label: 'Minutes', value: 'minutes' },
+ { label: 'Mins', value: 'minutes' },
{ label: 'Hours', value: 'hours' },
{ label: 'Days', value: 'days' },
]
const beforeAfterOptions = [
- { label: 'Before Due', value: 'before' },
+ { label: 'Before', value: 'before' },
{ label: 'On Due', value: 'ondue' },
- { label: 'After Due', value: 'after' },
+ { label: 'After', value: 'after' },
]
function getRelativeLabel(notification) {
- const { amount, unit, when } = notification
-
- // For "On Due" notification
- if (when === 'ondue') {
+ const { value, unit, type } = notification
+ if (type === 'ondue') {
return 'On due date'
}
-
- // For before/after notifications
- return `${amount} ${unit} ${when === 'before' ? 'before' : 'after'} due`
+ return `${value} ${unit} ${type === 'before' ? 'before' : 'after'} due`
}
-const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
+const NotificationTemplate = ({
+ maxNotifications = 5,
+ onChange,
+ value,
+ showTimeline = true,
+}) => {
const [templateName, setTemplateName] = useState(
value?.name || 'New Notification Template',
)
- const [notifications, setNotifications] = useState([
- value?.notifications || {
- amount: 0,
- when: 'ondue',
- },
- ])
- const [error, setError] = useState(null)
+ 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({})
- // Use useCallback to memoize the function
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.when !== b.when) {
- // Before Due always comes first
- if (a.when === 'before') return -1
- if (b.when === 'before') return 1
+ 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.when === 'ondue') return -1
- if (b.when === 'ondue') return 1
-
- // Default case (should not be reached with our options)
+ if (a.type === 'ondue') return -1
+ if (b.type === 'ondue') return 1
+ // DEFAULT CASE ( NOT SURE FOR FUTURE )?
return 0
}
- // Convert everything to minutes for consistent comparison
const getMinutes = notif => {
- const { amount, unit } = notif
- let minutes = amount
+ const { value, unit } = notif
+ let minutes = value
if (unit === 'hours') minutes *= 60
if (unit === 'days') minutes *= 24 * 60
return minutes
@@ -83,17 +82,16 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
// For After Due: sort in ascending order (closest to due first)
const aMinutes = getMinutes(a)
const bMinutes = getMinutes(b)
- return a.when === 'before' ? bMinutes - aMinutes : aMinutes - bMinutes
+ return a.type === 'before' ? bMinutes - aMinutes : aMinutes - bMinutes
})
- // Create index mapping
const indexMap = {}
sorted.forEach((item, index) => {
const originalIdx = notifications.findIndex(
n =>
- n.amount === item.amount &&
+ n.value === item.value &&
n.unit === item.unit &&
- n.when === item.when,
+ n.type === item.type,
)
indexMap[originalIdx] = index + 1
})
@@ -104,27 +102,25 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
// Sort notifications and update the index mapping
useEffect(() => {
updateNotificationIndices()
- // Clear any errors when notifications change
setError(null)
}, [updateNotificationIndices])
// Notify parent component of changes including the template name
useEffect(() => {
if (onChange) {
- onChange({ name: templateName, notifications })
+ onChange({ notifications })
}
- }, [templateName, notifications, onChange])
+ }, [notifications, onChange])
// Validates if a notification configuration already exists
const isDuplicate = (notification, currentIdx = -1) => {
return notifications.some((n, idx) => {
- // Skip comparing with itself when editing
if (idx === currentIdx) return false
return (
- n.amount === notification.amount &&
+ n.value === notification.value &&
n.unit === notification.unit &&
- n.when === notification.when
+ n.type === notification.type
)
})
}
@@ -136,17 +132,17 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
}
// Special handling for "On Due" option
- if (field === 'when' && value === 'ondue') {
+ if (field === 'type' && value === 'ondue') {
// Set default values for On Due (not applicable)
updatedNotification = {
...updatedNotification,
- amount: 0,
- unit: 'minutes', // default unit, not displayed to user
+ value: 1,
+ unit: 'minutes',
}
// Check if another notification is already "On Due"
const existingOnDue = notifications.findIndex(
- (n, i) => i !== idx && n.when === 'ondue',
+ (n, i) => i !== idx && n.type === 'ondue',
)
if (existingOnDue !== -1) {
@@ -157,7 +153,6 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
}
}
- // Check for duplicates when changing a notification
if (isDuplicate(updatedNotification, idx)) {
setError(
'This notification setting already exists. Please use a different timing.',
@@ -172,12 +167,9 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
setError(null)
}
- const handleNameChange = e => {
- setTemplateName(e.target.value)
- }
const addSmartNotification = type => {
if (notifications.length >= maxNotifications) return
-
+ setShowSaveDefault(true)
let newNotification
let suggestions = []
@@ -185,28 +177,28 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
case 'reminder':
// Suggest common reminder times that don't exist
suggestions = [
- { amount: 1, unit: 'hours', when: 'before' },
- { amount: 1, unit: 'days', when: 'before' },
- { amount: 30, unit: 'minutes', when: 'before' },
- { amount: 2, unit: 'hours', when: 'before' },
- { amount: 3, unit: 'days', when: 'before' },
+ { 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.when === 'ondue')) {
+ if (notifications.some(n => n.type === 'ondue')) {
setError('Only one "Due Alert" notification is allowed.')
return
}
- newNotification = { amount: 0, unit: 'minutes', when: 'ondue' }
+ newNotification = { value: 0, unit: 'minutes', type: 'ondue' }
break
case 'followup':
suggestions = [
- { amount: 1, unit: 'hours', when: 'after' },
- { amount: 1, unit: 'days', when: 'after' },
- { amount: 3, unit: 'days', when: 'after' },
- { amount: 1, unit: 'weeks', when: 'after' },
+ { 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
}
@@ -226,14 +218,14 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
(a, b) => {
// Convert everything to minutes for consistent comparison
const getMinutes = notif => {
- const { amount, unit, when } = notif
+ const { value, unit, type } = notif
// On Due is exactly at due date (0 minutes)
- if (when === 'ondue') return 0
+ if (type === 'ondue') return 0
- let minutes = amount
+ let minutes = value
if (unit === 'hours') minutes *= 60
if (unit === 'days') minutes *= 24 * 60
- return when === 'before' ? -minutes : minutes
+ return type === 'before' ? -minutes : minutes
}
return getMinutes(a) - getMinutes(b)
@@ -249,20 +241,19 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
setNotifications(updated)
onChange && onChange(updated)
}
- // Visualization: improved timeline with better scaling
const renderTimeline = () => {
// Sort notifications chronologically
const sorted = [...notifications].sort((a, b) => {
// Convert everything to minutes for consistent comparison
const getMinutes = notif => {
- const { amount, unit, when } = notif
+ const { value, unit, type } = notif
// On Due is exactly at due date (0 minutes)
- if (when === 'ondue') return 0
+ if (type === 'ondue') return 0
- let minutes = amount
+ let minutes = value
if (unit === 'hours') minutes *= 60
if (unit === 'days') minutes *= 24 * 60
- return when === 'before' ? -minutes : minutes
+ return type === 'before' ? -minutes : minutes
}
return getMinutes(a) - getMinutes(b)
@@ -274,27 +265,24 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
// Find the original index of this item in notifications array
const originalIdx = notifications.findIndex(
n =>
- n.amount === item.amount &&
+ n.value === item.value &&
n.unit === item.unit &&
- n.when === item.when,
+ n.type === item.type,
)
notificationIndexMap[originalIdx] = index + 1
}) // Get min and max notification times for dynamic scaling
const minutesValues = sorted.map(n => {
- // On Due is exactly at due date (0 minutes)
- if (n.when === 'ondue') return 0
+ if (n.type === 'ondue') return 0
- let minutes = n.amount
+ let minutes = n.value
if (n.unit === 'hours') minutes *= 60
if (n.unit === 'days') minutes *= 24 * 60
- return n.when === 'before' ? -minutes : minutes
+ return n.type === 'before' ? -minutes : minutes
})
- // Find min (before) and max (after) notification times
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
- // Dynamic scaling based on notification range
const getPositionPercent = minutes => {
// Due date is always at center (50%)
if (minutes === 0) return 50
@@ -314,7 +302,7 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
return (
-
+
Notification Timeline
{
display: 'flex',
flexDirection: 'column',
position: 'relative',
- height: 100,
+ height: 90,
bgcolor: 'background.level1',
borderRadius: 'md',
p: 2,
@@ -375,11 +363,11 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
{sorted.map((n, i) => {
// Convert to minutes for consistent scale
let minutes = 0
- if (n.when !== 'ondue') {
- minutes = n.amount
+ if (n.type !== 'ondue') {
+ minutes = n.value
if (n.unit === 'hours') minutes *= 60
if (n.unit === 'days') minutes *= 24 * 60
- if (n.when === 'before') minutes = -minutes
+ if (n.type === 'before') minutes = -minutes
}
// On Due notifications are always at the due date (0 minutes)
@@ -394,9 +382,9 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
left: `${percent}%`,
transform: 'translateX(-50%)',
color:
- n.when === 'before'
+ n.type === 'before'
? 'primary.600'
- : n.when === 'ondue'
+ : n.type === 'ondue'
? 'warning.600'
: 'success.600',
display: 'flex',
@@ -418,11 +406,11 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
size={'sm'}
variant={'solid'}
color={
- n.when === 'before'
- ? 'primary'
- : n.when === 'ondue'
+ n.type === 'before'
+ ? 'success'
+ : n.type === 'ondue'
? 'warning'
- : 'success'
+ : 'danger'
}
sx={{
'--Badge-paddingX': '4px',
@@ -452,22 +440,24 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
return (
-
+ {/*
Schedule Name
-
+ */}
{/* Template Name Field */}
-
+ {/*
Template Name
@@ -477,7 +467,7 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
placeholder='Enter template name'
sx={{ width: '100%' }}
/>
-
+ */}
{error && (
{
sx={{
display: 'flex',
alignItems: 'center',
- justifyContent: 'center',
- width: 30,
- mr: 1,
+ justifyContent: 'start',
+ width: 18,
+
flexShrink: 0,
}}
>
@@ -512,23 +502,23 @@ const NotificationTemplate = ({ maxNotifications = 5, onChange, value }) => {
sx={{
'--Badge-minHeight': '20px',
'--Badge-fontSize': '0.75rem',
+ // centering the badge:
}}
color={
- n.when === 'before'
- ? 'primary'
- : n.when === 'ondue'
+ n.type === 'before'
+ ? 'success'
+ : n.type === 'ondue'
? 'warning'
- : 'success'
+ : 'danger'
}
>
-
{/* Empty box to attach badge to */}
- {renderTimeline()}
+ {showSaveDefault && (
+ }
+ onClick={() => {
+ localStorage.setItem(
+ 'defaultNotificationTemplate',
+ JSON.stringify(notifications),
+ )
+ setShowSaveDefault(false)
+ }}
+ >
+ Save as Default for Future Tasks
+
+ )}
+
+ {showTimeline && renderTimeline()}
)
}
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/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 => (