Skip to content

Commit

Permalink
fix(streak-count): Fix streak count calculation.
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
ZL-Asica committed Nov 8, 2024
1 parent b446aac commit 4bef153
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 70 deletions.
1 change: 0 additions & 1 deletion src/contexts/UserContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import { createContext, useContext, useEffect, useState } from 'react'
/**
* @typedef {Object} Streak
* @property {Object.<string, number>} completedDays - Map of dates (as strings) to their completion counts.
* @property {number} count - Current streak count.
*/

/**
Expand Down
40 changes: 6 additions & 34 deletions src/hooks/useGoalsUpdater.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -105,6 +105,9 @@ describe('useGoalsUpdater', () => {
],
})

// should be empty Array
expect(user.streak).toEqual([])

await goalsUpdater.toggleTaskCompletion(
goalIndex,
microGoalIndex,
Expand All @@ -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 () => {
Expand Down
6 changes: 2 additions & 4 deletions src/pages/Streak.jsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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(
Expand Down
69 changes: 38 additions & 31 deletions src/utils/streakUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @ts-check
import dayjs from 'dayjs'

/**
* Gets the current date in Chicago timezone (formatted as YYYY-MM-DD).
Expand All @@ -13,49 +14,55 @@ export const getChicagoDate = () => {
}

/**
* Streak data type for a user.
* @typedef {Object} Streak
* @property {Object.<string, number>} 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.<string, number>, 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 }
}

0 comments on commit 4bef153

Please sign in to comment.