diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx index 4ed9bde..4154abb 100644 --- a/src/hooks/useAuth.jsx +++ b/src/hooks/useAuth.jsx @@ -36,6 +36,10 @@ export const AuthProvider = ({ children }) => { const login = async credentials => { setIsLoading(true) try { + // Ensure apiClient is initialized with the correct URL + await apiClient.init() + const currentBaseURL = apiClient.getApiURL() + const isNative = typeof window !== 'undefined' && window.Capacitor?.isNativePlatform?.() @@ -50,7 +54,7 @@ export const AuthProvider = ({ children }) => { config.credentials = 'include' } - const response = await fetch(`${baseURL}/auth/login`, config) + const response = await fetch(`${currentBaseURL}/auth/login`, config) if (!response.ok) { const error = await response.json() diff --git a/src/queries/ResourceQueries.jsx b/src/queries/ResourceQueries.jsx index 8f79f31..343a839 100644 --- a/src/queries/ResourceQueries.jsx +++ b/src/queries/ResourceQueries.jsx @@ -13,7 +13,7 @@ const isTokenValid = () => { } export const useResource = () => { - const { data, isLoading, error } = useQuery({ + const { data, isLoading, error, refetch } = useQuery({ queryKey: ['resource'], queryFn: async () => { const response = await GetResource() @@ -22,7 +22,6 @@ export const useResource = () => { staleTime: 6 * 60 * 60 * 1000, // 6 hours in milliseconds refetchOnWindowFocus: false, refetchOnReconnect: false, - enabled: isTokenValid(), // Only run query when we have a valid token }) - return { data, isLoading, error } + return { data, isLoading, error, refetch } } diff --git a/src/utils/ApiClient.js b/src/utils/ApiClient.js index aaa588c..1fa60af 100644 --- a/src/utils/ApiClient.js +++ b/src/utils/ApiClient.js @@ -16,12 +16,12 @@ class ApiClient { this.refreshCooldown = 3 * 1000 // 3 seconds in milliseconds } - async init() { + async init(force = false) { if (this.initPromise) { return this.initPromise } - if (this.initialized) { + if (this.initialized && !force) { return Promise.resolve() } diff --git a/src/utils/ChoreCardHelpers.jsx b/src/utils/ChoreCardHelpers.jsx new file mode 100644 index 0000000..ca87966 --- /dev/null +++ b/src/utils/ChoreCardHelpers.jsx @@ -0,0 +1,62 @@ +import moment from 'moment' + +/** + * Get the text to display for a chore's due date + * @param {string|null} nextDueDate - The next due date of the chore + * @param {Object} chore - The chore object (needed for nextDueDate null check) + * @returns {string} The formatted due date text + */ +export const getDueDateChipText = (nextDueDate, chore) => { + if (chore?.nextDueDate === null || nextDueDate === null) return 'No Due Date' + + const dueDate = moment(nextDueDate) + const diff = moment(nextDueDate).diff(moment(), 'hours') + + // if seconds and minutes set to 59, treat as no time (date only) + if (dueDate.seconds() === 59 && dueDate.minutes() === 59) { + if (diff < 0) { + // For overdue dates, show calendar format for recent dates + const absDiff = Math.abs(diff) + if (absDiff <= 48) { + return ( + 'Overdue ' + + moment(nextDueDate).calendar().split(' at ')[0].toLowerCase() + ) + } + return 'Overdue ' + dueDate.fromNow() + } + // if due in next 48 hours, show calendar format without time (e.g., "Tomorrow") + if (diff < 48 && diff > 0) { + return moment(nextDueDate).calendar().split(' at ')[0] + } + // if due date is after 48 hours, show it in format: Due in 3 days + return 'Due ' + dueDate.fromNow() + } + + // if due in next 48 hours, we should show it in this format: Tomorrow 11:00 AM + if (diff < 48 && diff > 0) { + return moment(nextDueDate).calendar().replace(' at', '') + } + return 'Due ' + moment(nextDueDate).fromNow() +} + +/** + * Get the color to use for a chore's due date chip + * @param {string|null} nextDueDate - The next due date of the chore + * @param {Object} chore - The chore object (needed for nextDueDate null check) + * @returns {string} The color name for the chip + */ +export const getDueDateChipColor = (nextDueDate, chore) => { + if (chore?.nextDueDate === null || 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' +} diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx index c29e96a..4335705 100644 --- a/src/utils/Fetcher.jsx +++ b/src/utils/Fetcher.jsx @@ -35,7 +35,8 @@ const createChore = userID => { }).then(response => response.json()) } -const signUp = (username, password, displayName, email) => { +const signUp = async (username, password, displayName, email) => { + await apiClient.init(true) const baseURL = apiManager.getApiURL() return fetch(`${baseURL}/auth/`, { method: 'POST', @@ -46,7 +47,8 @@ const signUp = (username, password, displayName, email) => { }) } -const UpdatePassword = newPassword => { +const UpdatePassword = async newPassword => { + await apiClient.init(true) const baseURL = apiManager.getApiURL() return fetch(`${baseURL}/users/change_password`, { method: 'PUT', @@ -55,7 +57,8 @@ const UpdatePassword = newPassword => { }) } -const login = (username, password) => { +const login = async (username, password) => { + await apiClient.init(true) const baseURL = apiManager.getApiURL() return fetch(`${baseURL}/auth/login`, { headers: { @@ -66,7 +69,8 @@ const login = (username, password) => { }) } -const logout = () => { +const logout = async () => { + await apiClient.init(true) const baseURL = apiManager.getApiURL() const isNative = typeof window !== 'undefined' && window.Capacitor?.isNativePlatform?.() @@ -476,6 +480,7 @@ const GetLabels = async () => { } const GetResource = async () => { + await apiClient.init() const basedURL = apiManager.getApiURL() const resp = await fetch(`${basedURL}/resource`, { method: 'GET', diff --git a/src/views/Authorization/Authenticating.jsx b/src/views/Authorization/Authenticating.jsx index e811b45..693fd4b 100644 --- a/src/views/Authorization/Authenticating.jsx +++ b/src/views/Authorization/Authenticating.jsx @@ -2,6 +2,7 @@ import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy' import { useEffect, useState } from 'react' import Logo from '../../Logo' +import { Capacitor } from '@capacitor/core' import Cookies from 'js-cookie' import { useRef } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' @@ -42,7 +43,7 @@ const AuthenticationLoading = () => { }) }) } - const handleOAuth2 = () => { + const handleOAuth2 = async () => { // get provider from params: const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') @@ -58,8 +59,11 @@ const AuthenticationLoading = () => { } if (code) { + await apiClient.init() const baseURL = apiClient.getApiURL() - + const redirectURI = Capacitor.isNativePlatform() + ? 'donetick://auth/oauth2' + : `${window.location.origin}/auth/oauth2` fetch(`${baseURL}/auth/oauth2/callback`, { method: 'POST', headers: { @@ -68,7 +72,7 @@ const AuthenticationLoading = () => { body: JSON.stringify({ code, state: returnedState, - redirect_uri: `${window.location.origin}/auth/oauth2`, + redirect_uri: redirectURI, }), }).then(response => { if (response.status === 200) { diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx index 229cd12..6ce8b7b 100644 --- a/src/views/Authorization/LoginSettings.jsx +++ b/src/views/Authorization/LoginSettings.jsx @@ -4,10 +4,12 @@ import React from 'react' import { useNavigate } from 'react-router-dom' import { API_URL } from '../../Config' import Logo from '../../Logo' +import { useResource } from '../../queries/ResourceQueries' import { useNotification } from '../../service/NotificationProvider' import { apiClient } from '../../utils/ApiClient' const LoginSettings = () => { const Navigate = useNavigate() + const { refetch: refetchResource } = useResource() const [serverURL, setServerURL] = React.useState('') const { showError } = useNotification() @@ -114,8 +116,12 @@ const LoginSettings = () => { Preferences.set({ key: 'customServerUrl', value: serverURL, - }).then(() => { - apiClient.customServerURL = serverURL + '/api/v1' + }).then(async () => { + // apiClient.customServerURL = serverURL + '/api/v1's + // Force re-initialization to reload from Preferences + await apiClient.init(true) + // refetch resource queries to update the API URL + refetchResource() Navigate('/login') }) }} diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 5371dcf..98c413e 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -256,7 +256,7 @@ const ChoreEdit = () => { // Default to end of day (23:59:59) in user's timezone const endOfDay = moment(dateValue) .endOf('day') - .format('YYYY-MM-DDTHH:mm:00') + .format('YYYY-MM-DDTHH:mm:59') setDueDate(endOfDay) } } diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index 0da767c..c7c2b9d 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -23,6 +23,10 @@ import { import moment from 'moment' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useUserProfile } from '../../queries/UserQueries.jsx' +import { + getDueDateChipColor, + getDueDateChipText, +} from '../../utils/ChoreCardHelpers.jsx' import { notInCompletionWindow } from '../../utils/Chores.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import Priorities from '../../utils/Priorities' @@ -62,28 +66,6 @@ const ChoreCard = ({ ) } - const getDueDateChipText = nextDueDate => { - if (chore.nextDueDate === null) return 'No Due Date' - // if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM - const diff = moment(nextDueDate).diff(moment(), 'hours') - if (diff < 48 && diff > 0) { - return moment(nextDueDate).calendar().replace(' at', '') - } - return 'Due ' + moment(nextDueDate).fromNow() - } - const getDueDateChipColor = 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 getRecurrentChipText = chore => { // if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata: const metadata = @@ -216,9 +198,9 @@ const ChoreCard = ({ zIndex: 3, left: 10, }} - color={getDueDateChipColor(chore.nextDueDate)} + color={getDueDateChipColor(chore.nextDueDate, chore)} > - {getDueDateChipText(chore.nextDueDate)} + {getDueDateChipText(chore.nextDueDate, chore)} { - if (chore.nextDueDate === null) return 'No Due Date' - // if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM - const diff = moment(nextDueDate).diff(moment(), 'hours') - if (diff < 48 && diff > 0) { - return moment(nextDueDate).calendar().replace(' at', '') - } - - 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: @@ -509,7 +491,7 @@ const CompactChoreCard = ({ - {getDueDateText(chore.nextDueDate)} + {getDueDateChipText(chore.nextDueDate, chore)}