From d1ad6a8e562a7dba2e9f096d027db042fac58928 Mon Sep 17 00:00:00 2001 From: WelldoneM Date: Thu, 7 Nov 2024 16:48:37 -0600 Subject: [PATCH 1/8] making the dates to all be the color black and adding back button to streak page --- src/components/common/Header.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx index 8a13539..01bb8b6 100644 --- a/src/components/common/Header.jsx +++ b/src/components/common/Header.jsx @@ -22,9 +22,8 @@ const Header = () => { // State for Dialog visibility const [openConfirmDialog, setOpenConfirmDialog] = useState(false) - // Show back button only on pages other than Home and Streak - const showBackButton = - location.pathname !== '/' && location.pathname !== '/streak' + // Show back button only on pages other than Home + const showBackButton = location.pathname !== '/' // Streak count (There may not have a user logged in) const streakCount = user?.streak?.count || 0 From 0c44e521a5b849b05148cb6876e5336a14068b80 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:57:22 -0600 Subject: [PATCH 2/8] revert(Header): Remove return for steak page --- src/components/common/Header.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx index 01bb8b6..8a13539 100644 --- a/src/components/common/Header.jsx +++ b/src/components/common/Header.jsx @@ -22,8 +22,9 @@ const Header = () => { // State for Dialog visibility const [openConfirmDialog, setOpenConfirmDialog] = useState(false) - // Show back button only on pages other than Home - const showBackButton = location.pathname !== '/' + // Show back button only on pages other than Home and Streak + const showBackButton = + location.pathname !== '/' && location.pathname !== '/streak' // Streak count (There may not have a user logged in) const streakCount = user?.streak?.count || 0 From 0416c5b69e3d3a9c8f14b3680857070ee437a273 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:17:25 -0600 Subject: [PATCH 3/8] style(streak): Disable clicking on specific date. --- src/pages/Streak.jsx | 2 ++ src/styles/StreakPage.css | 49 +++++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/pages/Streak.jsx b/src/pages/Streak.jsx index c09dc04..6e52415 100644 --- a/src/pages/Streak.jsx +++ b/src/pages/Streak.jsx @@ -66,9 +66,11 @@ const Streak = () => { true} tileContent={({ date, view }) => view === 'month' ? getTileIcon(date) : null } + className='custom-calendar' /> diff --git a/src/styles/StreakPage.css b/src/styles/StreakPage.css index 7f6a438..0ac2175 100644 --- a/src/styles/StreakPage.css +++ b/src/styles/StreakPage.css @@ -1,31 +1,36 @@ -/* Ensure all calendar dates have the color black */ +/* Ensure all calendar dates have a consistent black color, including weekends */ .react-calendar__month-view__days__day--weekend { - color: black !important; /* Override weekend color */ - } + color: black !important; /* Override the weekend default color */ +} -/* Custom weekday headers */ +/* Style weekday headers with bold, capitalized, and black text */ .react-calendar__month-view__weekdays__weekday { - text-transform: capitalize; - font-weight: bold; - color: black; - font-size: 0.8rem; + text-transform: capitalize; + font-weight: bold; + color: black; + font-size: 0.8rem; } -/* Larger navigation arrows */ +/* Enlarge navigation arrows for better visibility */ .react-calendar__navigation button { - font-size: 1rem; - color: black; + font-size: 1rem; + color: black; } -/* Animation for FireIcon */ -@keyframes burn-animation { - - 0%, - 100% { - transform: scale(1); - } +/* Remove the default background color */ +button.react-calendar__tile.react-calendar__month-view__days__day { + color: inherit; + background-color: #fff; + pointer-events: none; +} - 50% { - transform: scale(1.2); - } -} \ No newline at end of file +/* Animation for FireIcon, creating a pulsing "burn" effect */ +@keyframes burn-animation { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } +} From b446aac54bebce58dac3c65222f01d7bd97768e2 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:18:48 -0600 Subject: [PATCH 4/8] refactor(updateHelper): Only initialize new streak object when need to update. --- src/hooks/useGoalsUpdater/updateHelpers.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/hooks/useGoalsUpdater/updateHelpers.js b/src/hooks/useGoalsUpdater/updateHelpers.js index dd8648d..93679ec 100644 --- a/src/hooks/useGoalsUpdater/updateHelpers.js +++ b/src/hooks/useGoalsUpdater/updateHelpers.js @@ -8,13 +8,12 @@ export const updateGoalsAndStreak = async ( message = 'Update successful.', countChange = 0 ) => { - const updatedStreak = - countChange !== 0 - ? updateStreakDays(userContext.user, countChange) - : userContext.user.streak + // Only update streak if countChange is non-zero const updatedProfile = { goals: updatedGoals, - ...(countChange !== 0 && { streak: updatedStreak }), + ...(countChange !== 0 && { + streak: updateStreakDays(userContext.user, countChange), + }), } try { From 4bef15371fa194435ecd6e55d44a35fd3eae3eb0 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:19:48 -0600 Subject: [PATCH 5/8] fix(streak-count): Fix streak count calculation. - count will no longer be store in user.streaks, but will be calculated on the fly. - Update streakUtils to calculate streak count on the fly. - Update Streak component to use streakUtils to calculate streak count. - Update test accordingly. - Update UserContext'a type declaration accordingly. --- src/contexts/UserContext.jsx | 1 - src/hooks/useGoalsUpdater.test.js | 40 +++--------------- src/pages/Streak.jsx | 6 +-- src/utils/streakUtils.js | 69 +++++++++++++++++-------------- 4 files changed, 46 insertions(+), 70 deletions(-) diff --git a/src/contexts/UserContext.jsx b/src/contexts/UserContext.jsx index aab96d2..8705261 100644 --- a/src/contexts/UserContext.jsx +++ b/src/contexts/UserContext.jsx @@ -46,7 +46,6 @@ import { createContext, useContext, useEffect, useState } from 'react' /** * @typedef {Object} Streak * @property {Object.} completedDays - Map of dates (as strings) to their completion counts. - * @property {number} count - Current streak count. */ /** diff --git a/src/hooks/useGoalsUpdater.test.js b/src/hooks/useGoalsUpdater.test.js index 626fafc..db3b6be 100644 --- a/src/hooks/useGoalsUpdater.test.js +++ b/src/hooks/useGoalsUpdater.test.js @@ -21,7 +21,7 @@ describe('useGoalsUpdater', () => { profilePic: 'test-pic-url', name: 'Test User', goals: [], // Start with an empty goals array - streak: { count: 0, completedDays: [] }, + streak: [], } updateProfile = vi.fn(async (updates) => { @@ -105,6 +105,9 @@ describe('useGoalsUpdater', () => { ], }) + // should be empty Array + expect(user.streak).toEqual([]) + await goalsUpdater.toggleTaskCompletion( goalIndex, microGoalIndex, @@ -115,39 +118,8 @@ describe('useGoalsUpdater', () => { user.goals[goalIndex].microgoals[microGoalIndex].tasks[taskIndex] expect(task.completed).toBe(true) - expect(updateProfile).toHaveBeenCalledWith( - expect.objectContaining({ - goals: user.goals, - streak: expect.objectContaining({ - count: 1, - completedDays: expect.objectContaining({ - [new Date().toISOString().split('T')[0]]: 1, // Check today's date in YYYY-MM-DD format - }), - }), - }) - ) - }) - - it('should toggle microgoal expansion', async () => { - const goalIndex = 0 - const microGoalIndex = 0 - - // Add initial goal and microgoal to user data - user.goals.push({ - name: 'Goal 1', - category: '#000000', - microgoals: [{ name: 'MicroGoal 1', expanded: false, tasks: [] }], - }) - - await goalsUpdater.toggleExpansion(goalIndex, microGoalIndex) - - expect(user.goals[goalIndex].microgoals[microGoalIndex].expanded).toBe(true) - - expect(updateProfile).toHaveBeenCalledWith( - expect.objectContaining({ - goals: user.goals, - }) - ) + // check streak is not empty + expect(user.streak).not.toEqual([]) }) it('should delete a specified task', async () => { diff --git a/src/pages/Streak.jsx b/src/pages/Streak.jsx index 6e52415..f723e3c 100644 --- a/src/pages/Streak.jsx +++ b/src/pages/Streak.jsx @@ -1,6 +1,6 @@ import { useUser } from '@/contexts/UserContext' import '@/styles/StreakPage.css' -import { getChicagoDate } from '@/utils/streakUtils' +import { calculateStreakCount } from '@/utils/streakUtils' import FireIcon from '@mui/icons-material/Whatshot' import { Box, Typography } from '@mui/material' import { useMemo } from 'react' @@ -9,10 +9,8 @@ import 'react-calendar/dist/Calendar.css' const Streak = () => { const { user } = useUser() - - const streakCount = user.streak?.count || 0 const completedDays = user.streak?.completedDays || {} - const today = getChicagoDate() + const { today, streakCount } = calculateStreakCount(completedDays) // Cache the completed dates const completedDatesSet = useMemo( diff --git a/src/utils/streakUtils.js b/src/utils/streakUtils.js index 2ca396b..3753f69 100644 --- a/src/utils/streakUtils.js +++ b/src/utils/streakUtils.js @@ -1,4 +1,5 @@ // @ts-check +import dayjs from 'dayjs' /** * Gets the current date in Chicago timezone (formatted as YYYY-MM-DD). @@ -13,49 +14,55 @@ export const getChicagoDate = () => { } /** - * Streak data type for a user. - * @typedef {Object} Streak - * @property {Object.} completedDays - Map of date strings to their completion counts. - * @property {number} count - Current streak count. + * Completed days map with date-count pairs. + * @typedef {Object} completedDays + * @property {string} date - Date in YYYY-MM-DD format. + * @property {number} count - Number of completions for the date. */ /** - * User data type. - * @typedef {Object} User - * @property {Streak} streak - User's streak data. + * Updates the completed days for a user based on the current date. + * @param {completedDays} completedDays - The completed days map. + * @param {number} countChange - 1 or -1 to increment or decrement the count for the current date. + * @returns {completedDays} - Updated completedDays as a map of date-count pairs. */ - -/** - * Updates the streak count and completed days for a user. - * @param {User} user - The user object containing streak information. - * @param {number} countChange - The change to apply to the completion count for the current date. - * @returns {{completedDays: Object., count: number}} - Updated completedDays as a map of date-count pairs and the new streak count. - */ -export const updateStreakDays = (user, countChange) => { +export const updateStreakDays = (completedDays, countChange) => { const currentDate = getChicagoDate() - // { completedDays: { '2024-11-01': 1, '2024-11-02': 0 }, count: 1 } - const completedDays = user.streak?.completedDays || {} - const count = user.streak?.count || 0 - // Get the current count for the current date or initialize it to 0 const currentCount = completedDays[currentDate] || 0 // Update the count for the current date completedDays[currentDate] = Math.max(0, currentCount + countChange) - // Adjust the streak count based on changes to the current day's count - let newCount = count - if (currentCount === 0 && countChange > 0) { - // Increment streak if new positive count for the day - newCount++ - } else if (currentCount > 0 && completedDays[currentDate] === 0) { - // Decrement streak if current day count goes to 0 - newCount = Math.max(0, newCount - 1) - } + return completedDays +} + +/** + * Calculates the current streak count based on completedDays. + * @param {completedDays} completedDays - The map of date strings to completion counts. + * @returns {{ today: string, streakCount: number }} - The current date and streak count. + */ +export const calculateStreakCount = (completedDays) => { + const today = getChicagoDate() + const yesterday = dayjs(today).subtract(1, 'day').format('YYYY-MM-DD') + + // Sort dates in descending order, so we can check from yesterday backward + const dates = Object.keys(completedDays).sort( + (a, b) => new Date(b).getTime() - new Date(a).getTime() + ) - return { - completedDays, - count: newCount, + let streakCount = 0 + + for (const date of dates) { + if (date === today) continue // Skip today, only count until yesterday + + if (completedDays[date] > 0 && date <= yesterday) { + streakCount++ + } else { + break // Stop counting if a non-completed day is found + } } + + return { today, streakCount } } From 440dadf02f120a3a7730decefdfc2f7d6037921a Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:25:43 -0600 Subject: [PATCH 6/8] fix(streak-count): Fix params parsing. --- src/hooks/useGoalsUpdater/updateHelpers.js | 2 +- src/utils/streakUtils.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hooks/useGoalsUpdater/updateHelpers.js b/src/hooks/useGoalsUpdater/updateHelpers.js index 93679ec..83c0b5d 100644 --- a/src/hooks/useGoalsUpdater/updateHelpers.js +++ b/src/hooks/useGoalsUpdater/updateHelpers.js @@ -12,7 +12,7 @@ export const updateGoalsAndStreak = async ( const updatedProfile = { goals: updatedGoals, ...(countChange !== 0 && { - streak: updateStreakDays(userContext.user, countChange), + streak: updateStreakDays(userContext.user.streak, countChange), }), } diff --git a/src/utils/streakUtils.js b/src/utils/streakUtils.js index 3753f69..121c5c1 100644 --- a/src/utils/streakUtils.js +++ b/src/utils/streakUtils.js @@ -22,20 +22,20 @@ export const getChicagoDate = () => { /** * Updates the completed days for a user based on the current date. - * @param {completedDays} completedDays - The completed days map. + * @param {import('@/contexts/UserContext').Streak} streak - The user's streak object. * @param {number} countChange - 1 or -1 to increment or decrement the count for the current date. - * @returns {completedDays} - Updated completedDays as a map of date-count pairs. + * @returns {import('@/contexts/UserContext').Streak} The updated streak object. */ -export const updateStreakDays = (completedDays, countChange) => { +export const updateStreakDays = (streak, countChange) => { const currentDate = getChicagoDate() // Get the current count for the current date or initialize it to 0 - const currentCount = completedDays[currentDate] || 0 + const currentCount = streak.completedDays[currentDate] || 0 // Update the count for the current date - completedDays[currentDate] = Math.max(0, currentCount + countChange) + streak.completedDays[currentDate] = Math.max(0, currentCount + countChange) - return completedDays + return streak } /** From 84ad619feb335b23d7c4ab9ce6a3b34aa0a801e3 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:37:05 -0600 Subject: [PATCH 7/8] fix(strak-count): Handling when completedDays is null --- src/hooks/useGoalsUpdater.test.js | 7 ++----- src/utils/streakUtils.js | 8 ++++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/hooks/useGoalsUpdater.test.js b/src/hooks/useGoalsUpdater.test.js index db3b6be..8179e1e 100644 --- a/src/hooks/useGoalsUpdater.test.js +++ b/src/hooks/useGoalsUpdater.test.js @@ -105,9 +105,6 @@ describe('useGoalsUpdater', () => { ], }) - // should be empty Array - expect(user.streak).toEqual([]) - await goalsUpdater.toggleTaskCompletion( goalIndex, microGoalIndex, @@ -118,8 +115,8 @@ describe('useGoalsUpdater', () => { user.goals[goalIndex].microgoals[microGoalIndex].tasks[taskIndex] expect(task.completed).toBe(true) - // check streak is not empty - expect(user.streak).not.toEqual([]) + // Check user.streak.completedDays length is 1 + expect(Object.keys(user.streak.completedDays)).toHaveLength(1) }) it('should delete a specified task', async () => { diff --git a/src/utils/streakUtils.js b/src/utils/streakUtils.js index 121c5c1..46d7c4b 100644 --- a/src/utils/streakUtils.js +++ b/src/utils/streakUtils.js @@ -28,12 +28,16 @@ export const getChicagoDate = () => { */ export const updateStreakDays = (streak, countChange) => { const currentDate = getChicagoDate() + const completedDays = streak.completedDays || {} // Get the current count for the current date or initialize it to 0 - const currentCount = streak.completedDays[currentDate] || 0 + const currentCount = completedDays[currentDate] || 0 // Update the count for the current date - streak.completedDays[currentDate] = Math.max(0, currentCount + countChange) + streak.completedDays = { + ...completedDays, + [currentDate]: Math.max(0, currentCount + countChange), + } return streak } From 8c80a9316239f03f30d4de2f140bcd678f8fec09 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:46:16 -0600 Subject: [PATCH 8/8] fix(streak-count): fix streak count calculation logic. - Count and todayCount will update at every time fetching user profile. - Updates with task completion toggled. - Update Home.test.jsx to fully mock the UserContext to improve speed. --- src/contexts/UserContext.jsx | 10 +++- src/pages/Home.test.jsx | 56 ++++++++++--------- src/pages/Streak.jsx | 8 ++- src/utils/firebase/createUserProfile.js | 21 +++---- src/utils/streakUtils.js | 73 +++++++++++-------------- 5 files changed, 89 insertions(+), 79 deletions(-) diff --git a/src/contexts/UserContext.jsx b/src/contexts/UserContext.jsx index 8705261..1e346a2 100644 --- a/src/contexts/UserContext.jsx +++ b/src/contexts/UserContext.jsx @@ -43,9 +43,17 @@ import { createContext, useContext, useEffect, useState } from 'react' * @typedef {Object} GoalMap - A map of goal IDs to Goal objects. */ +/** + * @typedef {Object} CompletedDays + * @property {string} date - Date in YYYY-MM-DD format. + * @property {number} count - Number of completions for the date. + */ + /** * @typedef {Object} Streak - * @property {Object.} completedDays - Map of dates (as strings) to their completion counts. + * @property {CompletedDays} completedDays - Map of dates (as strings) to their completion counts. + * @property {number} count - Current streak count. + * @property {number} todayCount - Today's completion count. */ /** diff --git a/src/pages/Home.test.jsx b/src/pages/Home.test.jsx index d56dbaa..2c04ae2 100644 --- a/src/pages/Home.test.jsx +++ b/src/pages/Home.test.jsx @@ -3,33 +3,40 @@ import Home from '@/pages/Home' import { act, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, test, vi } from 'vitest' -// Mock `@contexts/UserContext` to control user profile and updates -vi.mock('@/contexts/UserContext', async () => { - const actual = await vi.importActual('@/contexts/UserContext') - let mockUserProfile = { - uid: '123', - profilePic: '', - name: 'Test User', - goals: [], - streak: [], - } +// Define mock data and functions directly +let mockUserProfile = { + uid: '123', + profilePic: '', + name: 'Test User', + goals: [], + streak: { completedDays: {}, count: 0 }, +} - return { - ...actual, - useUser: () => ({ - user: mockUserProfile, - loading: false, - updateProfile: vi.fn((updates) => { - mockUserProfile = { ...mockUserProfile, ...updates } - }), - }), - } +const mockUpdateProfile = vi.fn((updates) => { + mockUserProfile = { ...mockUserProfile, ...updates } }) +// Mock `@contexts/UserContext` to control user profile and updates +vi.mock('@/contexts/UserContext', () => ({ + UserProvider: ({ children }) => <>{children}, + useUser: () => ({ + user: mockUserProfile, + loading: false, + updateProfile: mockUpdateProfile, + }), +})) + describe('Home Screen - No Goals', () => { beforeEach(() => { - // Reset mockUserProfile for each test vi.clearAllMocks() + // Reset mockUserProfile to its initial state before each test + mockUserProfile = { + uid: '123', + profilePic: '', + name: 'Test User', + goals: [], + streak: { completedDays: {}, count: 0 }, + } }) test('Displays only "New Goal" field when there are no goals', async () => { @@ -42,11 +49,10 @@ describe('Home Screen - No Goals', () => { }) // Check if "New Goal" text field is present - const newGoalInput = screen.getByLabelText('New Goal') - expect(newGoalInput).not.to.be.null + expect(screen.getByLabelText('New Goal')).toBeTruthy() // Ensure "New Microgoal" and "New Task" fields are not present initially - expect(screen.queryByLabelText('New Microgoal')).to.be.null - expect(screen.queryByLabelText('New Task')).to.be.null + expect(screen.queryByLabelText('New Microgoal')).toBeNull() + expect(screen.queryByLabelText('New Task')).toBeNull() }) }) diff --git a/src/pages/Streak.jsx b/src/pages/Streak.jsx index f723e3c..888c7c2 100644 --- a/src/pages/Streak.jsx +++ b/src/pages/Streak.jsx @@ -1,6 +1,8 @@ +// @ts-check + import { useUser } from '@/contexts/UserContext' import '@/styles/StreakPage.css' -import { calculateStreakCount } from '@/utils/streakUtils' +import { getChicagoDate } from '@/utils/streakUtils' import FireIcon from '@mui/icons-material/Whatshot' import { Box, Typography } from '@mui/material' import { useMemo } from 'react' @@ -9,8 +11,10 @@ import 'react-calendar/dist/Calendar.css' const Streak = () => { const { user } = useUser() + + const streakCount = user.streak?.count || 0 const completedDays = user.streak?.completedDays || {} - const { today, streakCount } = calculateStreakCount(completedDays) + const today = getChicagoDate() // Cache the completed dates const completedDatesSet = useMemo( diff --git a/src/utils/firebase/createUserProfile.js b/src/utils/firebase/createUserProfile.js index 86e2255..5626b4b 100644 --- a/src/utils/firebase/createUserProfile.js +++ b/src/utils/firebase/createUserProfile.js @@ -1,6 +1,7 @@ // @ts-check import { db } from '@/utils/firebaseConfig' +import { calculateStreakCount } from '@/utils/streakUtils' import { doc, getDoc, setDoc, updateDoc } from 'firebase/firestore' /** @@ -18,7 +19,16 @@ export const fetchUserProfile = async (uid) => { return null } - return userSnapshot.data() + const profile = userSnapshot.data() + + const { count, todayCount } = calculateStreakCount( + profile.streak.completedDays + ) + + return { + ...profile, + streak: { ...profile.streak, count, todayCount }, + } } catch (error) { console.error('Error fetching user profile:', error) return null @@ -59,15 +69,6 @@ export const createFirstUserProfile = async (user) => { } } -/** - * Retrieves the user profile from Firestore by UID. - * @param {string} uid - User's UID. - * @returns {Promise} - The user profile data or null if not found. - */ -export const getUserProfile = async (uid) => { - return await fetchUserProfile(uid) -} - /** * Updates the user profile data in Firestore by UID. * @param {string} uid - User's UID. diff --git a/src/utils/streakUtils.js b/src/utils/streakUtils.js index 46d7c4b..b4102be 100644 --- a/src/utils/streakUtils.js +++ b/src/utils/streakUtils.js @@ -1,25 +1,20 @@ // @ts-check + import dayjs from 'dayjs' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' + +dayjs.extend(timezone) +dayjs.extend(utc) /** * Gets the current date in Chicago timezone (formatted as YYYY-MM-DD). * @returns {string} The current Chicago date as a string in YYYY-MM-DD format */ export const getChicagoDate = () => { - const chicagoTimeOffset = -6 * 60 // CST is UTC-6 - const chicagoDate = new Date( - new Date().getTime() + chicagoTimeOffset * 60 * 1000 - ) - return chicagoDate.toISOString().split('T')[0] + return dayjs().tz('America/Chicago').format('YYYY-MM-DD') } -/** - * Completed days map with date-count pairs. - * @typedef {Object} completedDays - * @property {string} date - Date in YYYY-MM-DD format. - * @property {number} count - Number of completions for the date. - */ - /** * Updates the completed days for a user based on the current date. * @param {import('@/contexts/UserContext').Streak} streak - The user's streak object. @@ -28,45 +23,41 @@ export const getChicagoDate = () => { */ export const updateStreakDays = (streak, countChange) => { const currentDate = getChicagoDate() - const completedDays = streak.completedDays || {} - - // Get the current count for the current date or initialize it to 0 - const currentCount = completedDays[currentDate] || 0 + const completedDays = { ...streak.completedDays } // Update the count for the current date - streak.completedDays = { - ...completedDays, - [currentDate]: Math.max(0, currentCount + countChange), - } + completedDays[currentDate] = Math.max( + 0, + (completedDays[currentDate] || 0) + countChange + ) + + const { count, todayCount } = calculateStreakCount(completedDays) - return streak + return { + ...streak, + completedDays, + count, + todayCount, + } } /** * Calculates the current streak count based on completedDays. - * @param {completedDays} completedDays - The map of date strings to completion counts. - * @returns {{ today: string, streakCount: number }} - The current date and streak count. + * @param {import ('@/contexts/UserContext').CompletedDays} completedDays - The user's completed days. + * @returns {{ count: number, todayCount: number }} - The current date and streak count. */ export const calculateStreakCount = (completedDays) => { const today = getChicagoDate() - const yesterday = dayjs(today).subtract(1, 'day').format('YYYY-MM-DD') - - // Sort dates in descending order, so we can check from yesterday backward - const dates = Object.keys(completedDays).sort( - (a, b) => new Date(b).getTime() - new Date(a).getTime() - ) - - let streakCount = 0 - - for (const date of dates) { - if (date === today) continue // Skip today, only count until yesterday - - if (completedDays[date] > 0 && date <= yesterday) { - streakCount++ - } else { - break // Stop counting if a non-completed day is found - } + const todayCount = completedDays[today] || 0 + // If the user completed a task today, the streak is at least 1 + let count = todayCount > 0 ? 1 : 0 + + // Start from yesterday to avoid counting today twice + let currentDate = dayjs(today).subtract(1, 'day').format('YYYY-MM-DD') + while (completedDays[currentDate] > 0) { + count++ + currentDate = dayjs(currentDate).subtract(1, 'day').format('YYYY-MM-DD') } - return { today, streakCount } + return { count, todayCount } }