From 0cee680fd1bf5ae45287f278453194aa60f1a93c Mon Sep 17 00:00:00 2001 From: WelldoneM Date: Fri, 1 Nov 2024 14:31:10 -0500 Subject: [PATCH 1/8] using react calendar --- package-lock.json | 102 +++++++++++++++++++++++++++++++++++ package.json | 1 + src/pages/Streak.jsx | 110 ++++++++++++++------------------------ src/styles/StreakPage.css | 33 +++++++++--- 4 files changed, 171 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfe9db4..fc41bd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mui/material": "^6.1.5", "firebase": "^11.0.1", "react": "^18.3.1", + "react-calendar": "^5.1.0", "react-dom": "^18.3.0", "react-router-dom": "^6.27.0" }, @@ -2793,6 +2794,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz", + "integrity": "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/acorn": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", @@ -4717,6 +4727,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-user-locale": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.2.tgz", + "integrity": "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==", + "license": "MIT", + "dependencies": { + "mem": "^8.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -6060,6 +6082,43 @@ "node": ">=10" } }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" + } + }, + "node_modules/mem/node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6390,6 +6449,15 @@ "node": ">= 0.8.0" } }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6766,6 +6834,31 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-5.1.0.tgz", + "integrity": "sha512-09o/rQHPZGEi658IXAJtWfra1N69D1eFnuJ3FQm9qUVzlzNnos1+GWgGiUeSs22QOpNm32aoVFOimq0p3Ug9Eg==", + "license": "MIT", + "dependencies": { + "@wojtekmaj/date-utils": "^1.1.3", + "clsx": "^2.0.0", + "get-user-locale": "^2.2.1", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -8051,6 +8144,15 @@ "node": ">=18" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 7d8b4ce..4d0a222 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@mui/material": "^6.1.5", "firebase": "^11.0.1", "react": "^18.3.1", + "react-calendar": "^5.1.0", "react-dom": "^18.3.0", "react-router-dom": "^6.27.0" }, diff --git a/src/pages/Streak.jsx b/src/pages/Streak.jsx index 817bc1e..f59db0f 100644 --- a/src/pages/Streak.jsx +++ b/src/pages/Streak.jsx @@ -1,92 +1,64 @@ import FireIcon from '@mui/icons-material/Whatshot'; -import { Box, Grid, Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import '@styles/StreakPage.css'; - -// Days of the week headers -const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; +import Calendar from 'react-calendar'; +import 'react-calendar/dist/Calendar.css'; const Streak = () => { - // Hard coded streak days + // Define the streak count and the hard-coded completed dates as strings const streakCount = 7; - const completedDays = [18, 19, 22, 23, 24, 25, 26, 27, 28]; - - //month start and end dates - const monthStart = new Date(2024, 9, 1); - const monthEnd = new Date(2024, 9 + 1, 0); + const completedDays = [ + '2024-10-18', + '2024-10-19', + '2024-10-22', + '2024-10-23', + '2024-10-24', + '2024-10-25', + '2024-10-26', + '2024-10-27', + '2024-10-28', + ]; - // Array to hold each day in October 2024 with the correct weekday alignment - const daysInCalendar = []; - const totalDays = monthEnd.getDate(); - const startDay = monthStart.getDay(); + // Get today's date for comparison + const today = new Date(); + const todayString = today.toISOString().split('T')[0]; - // Adding empty cells for days before the month starts - for (let i = 0; i < startDay; i++) { - daysInCalendar.push(null); - } - for (let day = 1; day <= totalDays; day++) { - daysInCalendar.push({ - day, - completed: completedDays.includes(day), - }); - } return ( + {/* Display the large fire icon and streak count */} {streakCount} Day Streak - - October 2024 - - - {/* Days of the Week Headers */} - - {daysOfWeek.map((day) => ( - - - {day} - - - ))} - + {/* Calendar with conditional fire icons */} + + { + const formattedDate = date.toISOString().split('T')[0]; + const isCompleted = completedDays.includes(formattedDate); + const isPastOrToday = formattedDate <= todayString; - {/* Calendar Grid */} - - {daysInCalendar.map((dayObj, index) => ( - - - {dayObj ? ( - <> - {dayObj.day} - - - ) : ( -   // Empty space for non-month dates - )} - - - ))} - + return view === 'month' && isPastOrToday ? ( + + + + ) : null; + }} + /> + ); }; diff --git a/src/styles/StreakPage.css b/src/styles/StreakPage.css index 81ea9e1..382aedc 100644 --- a/src/styles/StreakPage.css +++ b/src/styles/StreakPage.css @@ -1,13 +1,34 @@ +/* Centering and border removal from react calendar*/ +.react-calendar { + border: none; + width: 100%; + max-width: 500px; + color: black; +} + +/* Custom weekday headers */ +.react-calendar__month-view__weekdays__weekday { + text-transform: capitalize; + font-weight: bold; + color: black; + font-size: 0.9rem; +} + +/* Larger navigation arrows */ +.react-calendar__navigation button { + font-size: 1.2rem; + color: black; +} + +/* Animation for FireIcon */ @keyframes burn-animation { - 0% { + + 0%, + 100% { transform: scale(1); } 50% { - transform: scale(1.1); - } - - 100% { - transform: scale(1); + transform: scale(1.2); } } \ No newline at end of file From 0633fb704fba57f49f05c29ad6d9c9951d5e88d1 Mon Sep 17 00:00:00 2001 From: WelldoneM Date: Sun, 3 Nov 2024 11:48:13 -0600 Subject: [PATCH 2/8] feat(streak): Add automatic streak tracking based on daily task progress --- src/contexts/UserContext.jsx | 56 ++++++++++++++++++++++++- src/pages/Streak.jsx | 33 ++++++--------- src/styles/StreakPage.css | 12 +----- src/utils/calculateProgress.js | 18 ++++++-- src/utils/firebase/createUserProfile.js | 5 ++- src/utils/firebase/streakUtils.js | 47 +++++++++++++++++++++ 6 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 src/utils/firebase/streakUtils.js diff --git a/src/contexts/UserContext.jsx b/src/contexts/UserContext.jsx index ec3b6d7..2a46e17 100644 --- a/src/contexts/UserContext.jsx +++ b/src/contexts/UserContext.jsx @@ -1,19 +1,25 @@ import LoadingCircle from '@components/common/LoadingCircle'; +import { calculateProgress } from '@utils/calculateProgress'; import { signInWithGoogle } from '@utils/firebase/authUtils'; import { fetchUserProfile, updateUserProfile } from '@utils/firebase/createUserProfile'; +import { getStreakData, updateStreakData } from '@utils/firebase/streakUtils'; import { auth } from '@utils/firebaseConfig'; import { onAuthStateChanged, signOut } from 'firebase/auth'; import { createContext, useContext, useEffect, useState } from 'react'; -// Create UserContext +// Create the context const UserContext = createContext(); -// Custom hook to use UserContext +// Hook for accessing UserContext export const useUser = () => useContext(UserContext); export const UserProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); + const [completedDays, setCompletedDays] = useState([]); + const [streakCount, setStreakCount] = useState(0); + + const today = new Date().toISOString().split('T')[0]; // handle Sign-In const handleSignIn = async () => { @@ -44,6 +50,7 @@ export const UserProvider = ({ children }) => { const profile = await fetchUserProfile(firebaseUser.uid); if (profile) { setUser({ ...firebaseUser, ...profile }); + await fetchStreakData(firebaseUser.uid); } else { console.error('Failed to fetch user profile, logging out the user.'); await handleSignOut(); @@ -57,6 +64,46 @@ export const UserProvider = ({ children }) => { return () => unsubscribe(); }, []); + // Fetch streak data + const fetchStreakData = async (userId) => { + const { completedDays, streakCount } = await getStreakData(userId); + setCompletedDays(completedDays || []); + setStreakCount(streakCount || 0); + }; + + // Calculate streak based on consecutive days + const calculateStreak = (days) => { + const sortedDates = days.sort((a, b) => new Date(b) - new Date(a)); + let streak = 0; + for (let i = 0; i < sortedDates.length; i++) { + const dayDiff = (new Date(today) - new Date(sortedDates[i])) / (1000 * 60 * 60 * 24); + if (dayDiff === streak) { + streak++; + } else { + break; + } + } + return streak; + }; + + // Check for progress on goals and tasks today + const checkDailyProgress = async (goals) => { + const { completedDates } = calculateProgress(goals); + const progressMadeToday = completedDates.includes(today); + + if (progressMadeToday && !completedDays.includes(today)) { + const updatedDays = [...completedDays, today]; + const newStreakCount = calculateStreak(updatedDays); + + setCompletedDays(updatedDays); + setStreakCount(newStreakCount); + + if (user) { + await updateStreakData(user.uid, updatedDays, newStreakCount); + } + } + }; + // Function to update user profile const updateProfile = async (updates) => { if (user) { @@ -73,9 +120,14 @@ export const UserProvider = ({ children }) => { handleSignIn, handleSignOut, updateProfile, + streakCount, + completedDays, + checkDailyProgress, }} > {!loading ? children : } ); }; + +export default UserProvider; diff --git a/src/pages/Streak.jsx b/src/pages/Streak.jsx index f59db0f..bd04dfe 100644 --- a/src/pages/Streak.jsx +++ b/src/pages/Streak.jsx @@ -1,31 +1,23 @@ +import { useUser } from '@contexts/UserContext'; import FireIcon from '@mui/icons-material/Whatshot'; import { Box, Typography } from '@mui/material'; import '@styles/StreakPage.css'; +import { useEffect } from 'react'; import Calendar from 'react-calendar'; import 'react-calendar/dist/Calendar.css'; const Streak = () => { - // Define the streak count and the hard-coded completed dates as strings - const streakCount = 7; - const completedDays = [ - '2024-10-18', - '2024-10-19', - '2024-10-22', - '2024-10-23', - '2024-10-24', - '2024-10-25', - '2024-10-26', - '2024-10-27', - '2024-10-28', - ]; + const { streakCount, completedDays, checkDailyProgress } = useUser(); - // Get today's date for comparison - const today = new Date(); - const todayString = today.toISOString().split('T')[0]; + const today = new Date().toISOString().split('T')[0]; + + useEffect(() => { + //check for daily progress based on goals or tasks whenever component mounts or updates + checkDailyProgress(); + }, [checkDailyProgress]); return ( - {/* Display the large fire icon and streak count */} { {streakCount} Day Streak - {/* Calendar with conditional fire icons */} - + { const formattedDate = date.toISOString().split('T')[0]; const isCompleted = completedDays.includes(formattedDate); - const isPastOrToday = formattedDate <= todayString; + const isPastOrToday = formattedDate <= today; return view === 'month' && isPastOrToday ? ( diff --git a/src/styles/StreakPage.css b/src/styles/StreakPage.css index 382aedc..c98d851 100644 --- a/src/styles/StreakPage.css +++ b/src/styles/StreakPage.css @@ -1,22 +1,14 @@ -/* Centering and border removal from react calendar*/ -.react-calendar { - border: none; - width: 100%; - max-width: 500px; - color: black; -} - /* Custom weekday headers */ .react-calendar__month-view__weekdays__weekday { text-transform: capitalize; font-weight: bold; color: black; - font-size: 0.9rem; + font-size: 0.8rem; } /* Larger navigation arrows */ .react-calendar__navigation button { - font-size: 1.2rem; + font-size: 1rem; color: black; } diff --git a/src/utils/calculateProgress.js b/src/utils/calculateProgress.js index 1bb3608..38ebb62 100644 --- a/src/utils/calculateProgress.js +++ b/src/utils/calculateProgress.js @@ -1,9 +1,21 @@ export const calculateProgress = (items) => { // Calculate the progress of the goal tracker - const completed = items.reduce( + const completedTasks = items.reduce( (acc, item) => acc + item.tasks.filter((t) => t.completed).length, 0, ); - const total = items.reduce((acc, item) => acc + item.tasks.length, 0); - return total > 0 ? (completed / total) * 100 : 0; + const totalTasks = items.reduce((acc, item) => acc + item.tasks.length, 0); + + // Gather the unique dates of completed tasks + const completedDates = items.flatMap((item) => + item.tasks + .filter((task) => task.completed && task.completedDate) // Ensure `completedDate` is defined + .map((task) => task.completedDate), + ); + + // Return progress and the unique dates of completed tasks + return { + progress: totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0, + completedDates: [...new Set(completedDates)], // Unique dates only + }; }; diff --git a/src/utils/firebase/createUserProfile.js b/src/utils/firebase/createUserProfile.js index 85aa6ec..77dce91 100644 --- a/src/utils/firebase/createUserProfile.js +++ b/src/utils/firebase/createUserProfile.js @@ -35,7 +35,10 @@ export const createFirstUserProfile = async (user) => { profilePic: photoURL || '', name: displayName || '', goals: [], - streak: [], + streakData: { + completedDays: [], // Array to store dates when tasks were completed + streakCount: 0, // Initial streak count + }, }; try { diff --git a/src/utils/firebase/streakUtils.js b/src/utils/firebase/streakUtils.js new file mode 100644 index 0000000..cfa538d --- /dev/null +++ b/src/utils/firebase/streakUtils.js @@ -0,0 +1,47 @@ +import { db } from '@utils/firebaseConfig'; +import { doc, getDoc, updateDoc } from 'firebase/firestore'; + +/** + * Fetch streak data from Firestore by user ID. + * @param {string} uid - The user's unique identifier. + * @returns {object} - Streak data with `completedDays` and `streakCount`. + */ +export const getStreakData = async (uid) => { + try { + const userRef = doc(db, 'users', uid); + const userSnapshot = await getDoc(userRef); + + if (userSnapshot.exists()) { + const data = userSnapshot.data(); + return data.streakData || { completedDays: [], streakCount: 0 }; + } else { + console.error(`No streak data found for user ${uid}`); + return { completedDays: [], streakCount: 0 }; + } + } catch (error) { + console.error('Error fetching streak data:', error); + return { completedDays: [], streakCount: 0 }; + } +}; + +/** + * Update streak data in Firestore for the specified user. + * @param {string} uid - The user's unique identifier. + * @param {Array} completedDays - Array of dates when goals were completed. + * @param {number} streakCount - Current streak count. + * @returns {boolean} - Success status of the update. + */ +export const updateStreakData = async (uid, completedDays, streakCount) => { + try { + const userDocRef = doc(db, 'users', uid); + await updateDoc(userDocRef, { + 'streakData.completedDays': completedDays, + 'streakData.streakCount': streakCount, + }); + console.info('Streak data updated successfully.'); + return true; + } catch (error) { + console.error('Error updating streak data:', error); + return false; + } +}; From c9bb7dfef1a8b05a25142de2157b2f557b17a748 Mon Sep 17 00:00:00 2001 From: WelldoneM Date: Mon, 4 Nov 2024 18:27:11 -0600 Subject: [PATCH 3/8] calculate progress --- src/utils/calculateProgress.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/utils/calculateProgress.js b/src/utils/calculateProgress.js index 38ebb62..1bb3608 100644 --- a/src/utils/calculateProgress.js +++ b/src/utils/calculateProgress.js @@ -1,21 +1,9 @@ export const calculateProgress = (items) => { // Calculate the progress of the goal tracker - const completedTasks = items.reduce( + const completed = items.reduce( (acc, item) => acc + item.tasks.filter((t) => t.completed).length, 0, ); - const totalTasks = items.reduce((acc, item) => acc + item.tasks.length, 0); - - // Gather the unique dates of completed tasks - const completedDates = items.flatMap((item) => - item.tasks - .filter((task) => task.completed && task.completedDate) // Ensure `completedDate` is defined - .map((task) => task.completedDate), - ); - - // Return progress and the unique dates of completed tasks - return { - progress: totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0, - completedDates: [...new Set(completedDates)], // Unique dates only - }; + const total = items.reduce((acc, item) => acc + item.tasks.length, 0); + return total > 0 ? (completed / total) * 100 : 0; }; From d00380845408e9a509ee25d0948ffd1d1a9523b8 Mon Sep 17 00:00:00 2001 From: WelldoneM Date: Mon, 4 Nov 2024 18:43:09 -0600 Subject: [PATCH 4/8] adding streak count to header --- src/components/common/Header.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx index 88f01cc..1e5d6c2 100644 --- a/src/components/common/Header.jsx +++ b/src/components/common/Header.jsx @@ -7,7 +7,7 @@ import { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; const Header = () => { - const { user, handleSignIn, handleSignOut } = useUser(); + const { user, handleSignIn, handleSignOut, streakCount } = useUser(); const location = useLocation(); const navigate = useNavigate(); @@ -17,8 +17,6 @@ const Header = () => { // Show back button only on pages other than Home and Streak const showBackButton = location.pathname !== '/' && location.pathname !== '/streak'; - // Hard Coded streak count - const streakCount = 7; return ( From 7f1067eca8396b0682426670235d9415f19e6fe0 Mon Sep 17 00:00:00 2001 From: WelldoneM Date: Tue, 5 Nov 2024 08:04:08 -0600 Subject: [PATCH 5/8] calculating and updating the streak each day --- src/contexts/UserContext.jsx | 32 ++++++++++++++++++++------------ src/hooks/useGoalsUpdater.js | 5 +++++ src/utils/taskCompletion.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 src/utils/taskCompletion.js diff --git a/src/contexts/UserContext.jsx b/src/contexts/UserContext.jsx index 2a46e17..57d354e 100644 --- a/src/contexts/UserContext.jsx +++ b/src/contexts/UserContext.jsx @@ -1,9 +1,9 @@ import LoadingCircle from '@components/common/LoadingCircle'; -import { calculateProgress } from '@utils/calculateProgress'; import { signInWithGoogle } from '@utils/firebase/authUtils'; import { fetchUserProfile, updateUserProfile } from '@utils/firebase/createUserProfile'; import { getStreakData, updateStreakData } from '@utils/firebase/streakUtils'; import { auth } from '@utils/firebaseConfig'; +import { getCompletedDates } from '@utils/taskCompletion'; import { onAuthStateChanged, signOut } from 'firebase/auth'; import { createContext, useContext, useEffect, useState } from 'react'; @@ -19,8 +19,6 @@ export const UserProvider = ({ children }) => { const [completedDays, setCompletedDays] = useState([]); const [streakCount, setStreakCount] = useState(0); - const today = new Date().toISOString().split('T')[0]; - // handle Sign-In const handleSignIn = async () => { const userData = await signInWithGoogle(); @@ -64,7 +62,7 @@ export const UserProvider = ({ children }) => { return () => unsubscribe(); }, []); - // Fetch streak data + // Fetch streak data and initialize completedDays and streakCount const fetchStreakData = async (userId) => { const { completedDays, streakCount } = await getStreakData(userId); setCompletedDays(completedDays || []); @@ -73,11 +71,14 @@ export const UserProvider = ({ children }) => { // Calculate streak based on consecutive days const calculateStreak = (days) => { - const sortedDates = days.sort((a, b) => new Date(b) - new Date(a)); - let streak = 0; - for (let i = 0; i < sortedDates.length; i++) { - const dayDiff = (new Date(today) - new Date(sortedDates[i])) / (1000 * 60 * 60 * 24); - if (dayDiff === streak) { + if (!days.length) return 0; + + const sortedDates = days.map((date) => new Date(date)).sort((a, b) => b - a); + + let streak = 1; + for (let i = 1; i < sortedDates.length; i++) { + const diff = (sortedDates[i - 1] - sortedDates[i]) / (1000 * 60 * 60 * 24); + if (diff === 1) { streak++; } else { break; @@ -88,10 +89,10 @@ export const UserProvider = ({ children }) => { // Check for progress on goals and tasks today const checkDailyProgress = async (goals) => { - const { completedDates } = calculateProgress(goals); - const progressMadeToday = completedDates.includes(today); + const today = new Date().toISOString().split('T')[0]; + const completedDates = getCompletedDates(goals); - if (progressMadeToday && !completedDays.includes(today)) { + if (completedDates.includes(today) && !completedDays.includes(today)) { const updatedDays = [...completedDays, today]; const newStreakCount = calculateStreak(updatedDays); @@ -112,6 +113,13 @@ export const UserProvider = ({ children }) => { } }; + // Call `checkDailyProgress` once `user.goals` is loaded + useEffect(() => { + if (user && user.goals) { + checkDailyProgress(user.goals); // Call with `user.goals` once available + } + }, [user]); + return ( { console.error('Specified goal, microgoal, or task does not exist'); return; } + // Toggle the task completion status task.completed = !task.completed; + + // Set the completion date when the task is completed, clear if uncompleted + task.completionDate = task.completed ? new Date().toISOString().split('T')[0] : null; + // Update the user profile with updated goals await updateGoals(updatedGoals, 'Task completion status toggled successfully.'); }; diff --git a/src/utils/taskCompletion.js b/src/utils/taskCompletion.js new file mode 100644 index 0000000..395e1fb --- /dev/null +++ b/src/utils/taskCompletion.js @@ -0,0 +1,30 @@ +/** + * Get unique dates on which tasks have been completed + * @param {Array} goals - List of goal objects, each containing microgoals and tasks + * @returns {Array} - Unique dates when tasks were marked as completed + */ +export const getCompletedDates = (goals) => { + const completedDates = new Set(); + + // Check if goals is an array and iterate only if it exists + if (Array.isArray(goals)) { + goals.forEach((goal) => { + // Ensure microgoals is defined and an array + if (Array.isArray(goal.microgoals)) { + goal.microgoals.forEach((microGoal) => { + // Ensure tasks is defined and an array + if (Array.isArray(microGoal.tasks)) { + microGoal.tasks.forEach((task) => { + // Check for both task completion and valid completion date + if (task.completed && task.completionDate) { + completedDates.add(task.completionDate); + } + }); + } + }); + } + }); + } + + return Array.from(completedDates); +}; From ffb50ed092274d0c930e55d3f8cc77b4005ed06b Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:45:14 -0600 Subject: [PATCH 6/8] build: dependencies update. --- .node-version | 1 + package-lock.json | 368 ++++++++++++++++++++++++---------------------- package.json | 12 +- 3 files changed, 203 insertions(+), 178 deletions(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..fdb2eaa --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.11.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc41bd6..f8a901a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", - "@mui/icons-material": "^6.1.5", - "@mui/material": "^6.1.5", + "@mui/icons-material": "^6.1.6", + "@mui/material": "^6.1.6", "firebase": "^11.0.1", "react": "^18.3.1", "react-calendar": "^5.1.0", @@ -26,9 +26,9 @@ "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.0.1", "@vitejs/plugin-react": "^4.3.3", - "@vitest/coverage-v8": "^2.1.3", - "@vitest/ui": "^2.1.3", - "eslint": "^9.13.0", + "@vitest/coverage-v8": "^2.1.4", + "@vitest/ui": "^2.1.4", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.37.2", @@ -38,7 +38,7 @@ "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^4.1.0", "vite": "^5.4.10", - "vitest": "^2.1.3" + "vitest": "^2.1.4" } }, "node_modules/@ampproject/remapping": { @@ -302,9 +302,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz", - "integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -938,9 +938,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -1058,9 +1058,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", - "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", "dev": true, "license": "MIT", "engines": { @@ -1734,9 +1734,9 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", - "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1744,19 +1744,33 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", - "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.0", + "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1772,9 +1786,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", + "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1862,9 +1876,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.5.tgz", - "integrity": "sha512-3J96098GrC95XsLw/TpGNMxhUOnoG9NZ/17Pfk1CrJj+4rcuolsF2RdF3XAFTu/3a/A+5ouxlSIykzYz6Ee87g==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz", + "integrity": "sha512-nz1SlR9TdBYYPz4qKoNasMPRiGb4PaIHFkzLzhju0YVYS5QSuFF2+n7CsiHMIDcHv3piPu/xDWI53ruhOqvZwQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1872,12 +1886,12 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.5.tgz", - "integrity": "sha512-SbxFtO5I4cXfvhjAMgGib/t2lQUzcEzcDFYiRHRufZUeMMeXuoKaGsptfwAHTepYkv0VqcCwvxtvtWbpZLAbjQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.6.tgz", + "integrity": "sha512-5r9urIL2lxXb/sPN3LFfFYEibsXJUb986HhhIeu1gOcte460pwdSiEhBSxkAuyT8Dj7jvu9MjqSBmSumQELo8A==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7" + "@babel/runtime": "^7.26.0" }, "engines": { "node": ">=14.0.0" @@ -1887,7 +1901,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.1.5", + "@mui/material": "^6.1.6", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1898,16 +1912,16 @@ } }, "node_modules/@mui/material": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz", - "integrity": "sha512-rhaxC7LnlOG8zIVYv7BycNbWkC5dlm9A/tcDUp0CuwA7Zf9B9JP6M3rr50cNKxI7Z0GIUesAT86ceVm44quwnQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.6.tgz", + "integrity": "sha512-1yvejiQ/601l5AK3uIdUlAVElyCxoqKnl7QA+2oFB/2qYPWfRwDgavW/MoywS5Y2gZEslcJKhe0s2F3IthgFgw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/core-downloads-tracker": "^6.1.5", - "@mui/system": "^6.1.5", - "@mui/types": "^7.2.18", - "@mui/utils": "^6.1.5", + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.1.6", + "@mui/system": "^6.1.6", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.6", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", @@ -1926,7 +1940,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.5", + "@mui/material-pigment-css": "^6.1.6", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1953,13 +1967,13 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.5.tgz", - "integrity": "sha512-FJqweqEXk0KdtTho9C2h6JEKXsOT7MAVH2Uj3N5oIqs6YKxnwBn2/zL2QuYYEtj5OJ87rEUnCfFic6ldClvzJw==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.6.tgz", + "integrity": "sha512-ioAiFckaD/fJSnTrUMWgjl9HYBWt7ixCh7zZw7gDZ+Tae7NuprNV6QJK95EidDT7K0GetR2rU3kAeIR61Myttw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^6.1.5", + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.1.6", "prop-types": "^15.8.1" }, "engines": { @@ -1980,12 +1994,12 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.5.tgz", - "integrity": "sha512-tiyWzMkHeWlOoE6AqomWvYvdml8Nv5k5T+LDwOiwHEawx8P9Lyja6ZwWPU6xljwPXYYPT2KBp1XvMly7dsK46A==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.6.tgz", + "integrity": "sha512-I+yS1cSuSvHnZDBO7e7VHxTWpj+R7XlSZvTC4lS/OIbUNJOMMSd3UDP6V2sfwzAdmdDNBi7NGCRv2SZ6O9hGDA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7", + "@babel/runtime": "^7.26.0", "@emotion/cache": "^11.13.1", "@emotion/serialize": "^1.3.2", "@emotion/sheet": "^1.4.0", @@ -2014,16 +2028,16 @@ } }, "node_modules/@mui/system": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.5.tgz", - "integrity": "sha512-vPM9ocQ8qquRDByTG3XF/wfYTL7IWL/20EiiKqByLDps8wOmbrDG9rVznSE3ZbcjFCFfMRMhtxvN92bwe/63SA==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.6.tgz", + "integrity": "sha512-qOf1VUE9wK8syiB0BBCp82oNBAVPYdj4Trh+G1s+L+ImYiKlubWhhqlnvWt3xqMevR+D2h1CXzA1vhX2FvA+VQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/private-theming": "^6.1.5", - "@mui/styled-engine": "^6.1.5", - "@mui/types": "^7.2.18", - "@mui/utils": "^6.1.5", + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.1.6", + "@mui/styled-engine": "^6.1.6", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.6", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -2054,9 +2068,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.18", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", - "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -2068,13 +2082,13 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.5.tgz", - "integrity": "sha512-vp2WfNDY+IbKUIGg+eqX1Ry4t/BilMjzp6p9xO1rfqpYjH1mj8coQxxDfKxcQLzBQkmBJjymjoGOak5VUYwXug==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.6.tgz", + "integrity": "sha512-sBS6D9mJECtELASLM+18WUcXF6RH3zNxBRFeyCRg8wad6NbyNrdxLuwK+Ikvc38sTZwBzAz691HmSofLqHd9sQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/types": "^7.2.18", + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.19", "@types/prop-types": "^15.7.13", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -2626,21 +2640,21 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.3.tgz", - "integrity": "sha512-2OJ3c7UPoFSmBZwqD2VEkUw6A/tzPF0LmW0ZZhhB8PFxuc+9IBG/FaSM+RLEenc7ljzFvGN+G0nGQoZnh7sy2A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.4.tgz", + "integrity": "sha512-FPKQuJfR6VTfcNMcGpqInmtJuVXFSCd9HQltYncfR01AzXhLucMEtQ5SinPdZxsT5x/5BK7I5qFJ5/ApGCmyTQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.6", + "debug": "^4.3.7", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.11", - "magicast": "^0.3.4", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", "std-env": "^3.7.0", "test-exclude": "^7.0.1", "tinyrainbow": "^1.2.0" @@ -2649,8 +2663,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.3", - "vitest": "2.1.3" + "@vitest/browser": "2.1.4", + "vitest": "2.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2659,15 +2673,15 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz", - "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.4.tgz", + "integrity": "sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.3", - "@vitest/utils": "2.1.3", - "chai": "^5.1.1", + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -2675,22 +2689,21 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz", - "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.4.tgz", + "integrity": "sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.3", + "@vitest/spy": "2.1.4", "estree-walker": "^3.0.3", - "magic-string": "^0.30.11" + "magic-string": "^0.30.12" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.3", - "msw": "^2.3.5", + "msw": "^2.4.9", "vite": "^5.0.0" }, "peerDependenciesMeta": { @@ -2703,9 +2716,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", - "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.4.tgz", + "integrity": "sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -2716,13 +2729,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz", - "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.4.tgz", + "integrity": "sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.3", + "@vitest/utils": "2.1.4", "pathe": "^1.1.2" }, "funding": { @@ -2730,14 +2743,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", - "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.4.tgz", + "integrity": "sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.3", - "magic-string": "^0.30.11", + "@vitest/pretty-format": "2.1.4", + "magic-string": "^0.30.12", "pathe": "^1.1.2" }, "funding": { @@ -2745,49 +2758,49 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", - "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", + "integrity": "sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.0" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.3.tgz", - "integrity": "sha512-2XwTrHVJw3t9NYES26LQUYy51ZB8W4bRPgqUH2Eyda3kIuOlYw1ZdPNU22qcVlUVx4WKgECFQOSXuopsczuVjQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.4.tgz", + "integrity": "sha512-Zd9e5oU063c+j9N9XzGJagCLNvG71x/2tOme3Js4JEZKX55zsgxhJwUgLI8hkN6NjMLpdJO8d7nVUUuPGAA58Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.3", + "@vitest/utils": "2.1.4", "fflate": "^0.8.2", "flatted": "^3.3.1", "pathe": "^1.1.2", - "sirv": "^2.0.4", - "tinyglobby": "^0.2.6", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.9", "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.3" + "vitest": "2.1.4" } }, "node_modules/@vitest/utils": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz", - "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz", + "integrity": "sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.3", - "loupe": "^3.1.1", + "@vitest/pretty-format": "2.1.4", + "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -2804,9 +2817,9 @@ } }, "node_modules/acorn": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", - "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -3990,22 +4003,22 @@ } }, "node_modules/eslint": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", - "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", + "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.7.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.13.0", + "@eslint/js": "9.14.0", "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -4013,9 +4026,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4222,9 +4235,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4239,9 +4252,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4265,15 +4278,15 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4283,9 +4296,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4382,6 +4395,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7294,9 +7317,9 @@ } }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", "dev": true, "license": "MIT", "dependencies": { @@ -7305,7 +7328,7 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/slice-ansi": { @@ -8045,14 +8068,14 @@ } }, "node_modules/vite-node": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", - "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.4.tgz", + "integrity": "sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.6", + "debug": "^4.3.7", "pathe": "^1.1.2", "vite": "^5.0.0" }, @@ -8067,30 +8090,31 @@ } }, "node_modules/vitest": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz", - "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.3", - "@vitest/mocker": "2.1.3", - "@vitest/pretty-format": "^2.1.3", - "@vitest/runner": "2.1.3", - "@vitest/snapshot": "2.1.3", - "@vitest/spy": "2.1.3", - "@vitest/utils": "2.1.3", - "chai": "^5.1.1", - "debug": "^4.3.6", - "magic-string": "^0.30.11", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.4.tgz", + "integrity": "sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.4", + "@vitest/mocker": "2.1.4", + "@vitest/pretty-format": "^2.1.4", + "@vitest/runner": "2.1.4", + "@vitest/snapshot": "2.1.4", + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.7.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.0", - "tinypool": "^1.0.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.3", + "vite-node": "2.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8105,8 +8129,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.3", - "@vitest/ui": "2.1.3", + "@vitest/browser": "2.1.4", + "@vitest/ui": "2.1.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index 4d0a222..936623d 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", - "@mui/icons-material": "^6.1.5", - "@mui/material": "^6.1.5", + "@mui/icons-material": "^6.1.6", + "@mui/material": "^6.1.6", "firebase": "^11.0.1", "react": "^18.3.1", "react-calendar": "^5.1.0", @@ -40,9 +40,9 @@ "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.0.1", "@vitejs/plugin-react": "^4.3.3", - "@vitest/coverage-v8": "^2.1.3", - "@vitest/ui": "^2.1.3", - "eslint": "^9.13.0", + "@vitest/coverage-v8": "^2.1.4", + "@vitest/ui": "^2.1.4", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.37.2", @@ -52,6 +52,6 @@ "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^4.1.0", "vite": "^5.4.10", - "vitest": "^2.1.3" + "vitest": "^2.1.4" } } From dd412c0827a8f37a53a9ab0d710add00eafd4253 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:46:41 -0600 Subject: [PATCH 7/8] refactor(steaks): streamline streak and goal updates - Moved streak logic from context to streamlined streakUtils.js for better modularity - Removed redundant streakUtils and taskCompletion utils in firebase directory - Updated Streak.jsx to improve rendering logic and performance, optimized tileContent for calendar - Refactored useGoalsUpdater.js to consolidate goal and streak updates - Directly use context for streak in Header --- src/components/common/Header.jsx | 5 +- src/contexts/UserContext.jsx | 64 +----------------- src/hooks/useGoalsUpdater.js | 90 +++++++++++++++---------- src/pages/Streak.jsx | 60 ++++++++++------- src/utils/firebase/createUserProfile.js | 5 +- src/utils/firebase/streakUtils.js | 47 ------------- src/utils/streakUtils.js | 32 +++++++++ src/utils/taskCompletion.js | 30 --------- 8 files changed, 126 insertions(+), 207 deletions(-) delete mode 100644 src/utils/firebase/streakUtils.js create mode 100644 src/utils/streakUtils.js delete mode 100644 src/utils/taskCompletion.js diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx index 1e5d6c2..7664e21 100644 --- a/src/components/common/Header.jsx +++ b/src/components/common/Header.jsx @@ -7,7 +7,7 @@ import { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; const Header = () => { - const { user, handleSignIn, handleSignOut, streakCount } = useUser(); + const { user, handleSignIn, handleSignOut } = useUser(); const location = useLocation(); const navigate = useNavigate(); @@ -17,6 +17,9 @@ const Header = () => { // 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; + return ( diff --git a/src/contexts/UserContext.jsx b/src/contexts/UserContext.jsx index 57d354e..ec3b6d7 100644 --- a/src/contexts/UserContext.jsx +++ b/src/contexts/UserContext.jsx @@ -1,23 +1,19 @@ import LoadingCircle from '@components/common/LoadingCircle'; import { signInWithGoogle } from '@utils/firebase/authUtils'; import { fetchUserProfile, updateUserProfile } from '@utils/firebase/createUserProfile'; -import { getStreakData, updateStreakData } from '@utils/firebase/streakUtils'; import { auth } from '@utils/firebaseConfig'; -import { getCompletedDates } from '@utils/taskCompletion'; import { onAuthStateChanged, signOut } from 'firebase/auth'; import { createContext, useContext, useEffect, useState } from 'react'; -// Create the context +// Create UserContext const UserContext = createContext(); -// Hook for accessing UserContext +// Custom hook to use UserContext export const useUser = () => useContext(UserContext); export const UserProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - const [completedDays, setCompletedDays] = useState([]); - const [streakCount, setStreakCount] = useState(0); // handle Sign-In const handleSignIn = async () => { @@ -48,7 +44,6 @@ export const UserProvider = ({ children }) => { const profile = await fetchUserProfile(firebaseUser.uid); if (profile) { setUser({ ...firebaseUser, ...profile }); - await fetchStreakData(firebaseUser.uid); } else { console.error('Failed to fetch user profile, logging out the user.'); await handleSignOut(); @@ -62,49 +57,6 @@ export const UserProvider = ({ children }) => { return () => unsubscribe(); }, []); - // Fetch streak data and initialize completedDays and streakCount - const fetchStreakData = async (userId) => { - const { completedDays, streakCount } = await getStreakData(userId); - setCompletedDays(completedDays || []); - setStreakCount(streakCount || 0); - }; - - // Calculate streak based on consecutive days - const calculateStreak = (days) => { - if (!days.length) return 0; - - const sortedDates = days.map((date) => new Date(date)).sort((a, b) => b - a); - - let streak = 1; - for (let i = 1; i < sortedDates.length; i++) { - const diff = (sortedDates[i - 1] - sortedDates[i]) / (1000 * 60 * 60 * 24); - if (diff === 1) { - streak++; - } else { - break; - } - } - return streak; - }; - - // Check for progress on goals and tasks today - const checkDailyProgress = async (goals) => { - const today = new Date().toISOString().split('T')[0]; - const completedDates = getCompletedDates(goals); - - if (completedDates.includes(today) && !completedDays.includes(today)) { - const updatedDays = [...completedDays, today]; - const newStreakCount = calculateStreak(updatedDays); - - setCompletedDays(updatedDays); - setStreakCount(newStreakCount); - - if (user) { - await updateStreakData(user.uid, updatedDays, newStreakCount); - } - } - }; - // Function to update user profile const updateProfile = async (updates) => { if (user) { @@ -113,13 +65,6 @@ export const UserProvider = ({ children }) => { } }; - // Call `checkDailyProgress` once `user.goals` is loaded - useEffect(() => { - if (user && user.goals) { - checkDailyProgress(user.goals); // Call with `user.goals` once available - } - }, [user]); - return ( { handleSignIn, handleSignOut, updateProfile, - streakCount, - completedDays, - checkDailyProgress, }} > {!loading ? children : } ); }; - -export default UserProvider; diff --git a/src/hooks/useGoalsUpdater.js b/src/hooks/useGoalsUpdater.js index 51a2b58..08b35f1 100644 --- a/src/hooks/useGoalsUpdater.js +++ b/src/hooks/useGoalsUpdater.js @@ -1,40 +1,61 @@ import { useUser } from '@contexts/UserContext'; +import { updateStreakDays } from '@utils/streakUtils'; const useGoalsUpdater = () => { const { user, updateProfile } = useUser(); - // Update the goals in the user profile - const updateGoals = async (updatedGoals, message) => { + // Function to update both goals and optionally streak in the user profile + const updateGoalsAndStreak = async (updatedGoals, countChange = 0, message) => { try { - await updateProfile({ goals: updatedGoals }); + // If countChange is not 0, update the streak days + let updatedStreak = user.streak; + if (countChange !== 0) { + updatedStreak = updateStreakDays(user, countChange); + } + + // Combine the updated goals and streak + const updatedProfile = { + goals: updatedGoals, + ...(countChange !== 0 && { streak: updatedStreak }), + }; // Only update streak if countChange is not 0 + + // Update the user profile + await updateProfile(updatedProfile); console.log(message); } catch (error) { - console.error(`Error updating goals: ${message}`, error); + console.error(`Error updating goals and streak: ${message}`, error); } }; // Add a new goal, microgoal, or task - const addItem = async (goalIndex, microGoalIndex, newItem, itemType) => { + const addItem = async (newItem, itemType, goalIndex = undefined, microGoalIndex = undefined) => { const updatedGoals = [...user.goals]; - let target = updatedGoals[goalIndex]; - if (microGoalIndex !== undefined) { - target = target?.microgoals[microGoalIndex]; - } - - if (!target && itemType !== 'goal') { - console.error(`${itemType} does not exist`); - return; - } - if (itemType === 'task') { - target.tasks.push(newItem); - } else if (itemType === 'microgoal') { - target.microgoals.push(newItem); - } else if (itemType === 'goal') { - updatedGoals.push(newItem); + switch (itemType) { + case 'goal': + updatedGoals.push(newItem); + break; + case 'microgoal': + if (goalIndex === undefined) { + console.error('Goal index is required for adding a microgoal'); + return; + } + updatedGoals[goalIndex]?.microgoals.push(newItem); + break; + case 'task': + if (goalIndex === undefined || microGoalIndex === undefined) { + console.error('Both goal and microgoal indices are required for adding a task'); + return; + } + updatedGoals[goalIndex]?.microgoals[microGoalIndex]?.tasks.push(newItem); + break; + default: + console.error(`Unsupported item type: ${itemType}`); + return; } - await updateGoals( + // Update profile with goals and success message + await updateGoalsAndStreak( updatedGoals, `${itemType.charAt(0).toUpperCase() + itemType.slice(1)} added successfully.`, ); @@ -48,7 +69,7 @@ const useGoalsUpdater = () => { ? updatedGoals[goalIndex].microgoals[microGoalIndex] : updatedGoals[goalIndex]; target.expanded = !target.expanded; - await updateGoals(updatedGoals, 'Expansion toggled successfully.'); + await updateGoalsAndStreak(updatedGoals, 'Expansion toggled successfully.'); }; // Toggle the completion status of a task @@ -62,10 +83,11 @@ const useGoalsUpdater = () => { // Toggle the task completion status task.completed = !task.completed; - // Set the completion date when the task is completed, clear if uncompleted - task.completionDate = task.completed ? new Date().toISOString().split('T')[0] : null; - // Update the user profile with updated goals - await updateGoals(updatedGoals, 'Task completion status toggled successfully.'); + await updateGoalsAndStreak( + updatedGoals, + task.completed ? 1 : -1, + 'Task completion status toggled successfully.', + ); }; // Delete a goal, microgoal, or task @@ -80,22 +102,16 @@ const useGoalsUpdater = () => { updatedGoals.splice(goalIndex, 1); } - await updateGoals(updatedGoals, 'Item deleted successfully.'); + await updateGoalsAndStreak(updatedGoals, 'Item deleted successfully.'); }; return { - // Add a new goal, microgoal, or task - addGoal: (goalName) => - addItem(undefined, undefined, { name: goalName, expanded: false, microgoals: [] }, 'goal'), + // Add new goal, microgoal, or task + addGoal: (goalName) => addItem({ name: goalName, expanded: false, microgoals: [] }, 'goal'), addMicrogoal: (goalIndex, microGoalName) => - addItem( - goalIndex, - undefined, - { name: microGoalName, expanded: false, tasks: [] }, - 'microgoal', - ), + addItem({ name: microGoalName, expanded: false, tasks: [] }, 'microgoal', goalIndex), addTask: (goalIndex, microGoalIndex, taskName) => - addItem(goalIndex, microGoalIndex, { name: taskName, completed: false }, 'task'), + addItem({ name: taskName, completed: false }, 'task', goalIndex, microGoalIndex), // Delete deleteItem, diff --git a/src/pages/Streak.jsx b/src/pages/Streak.jsx index bd04dfe..a77fb94 100644 --- a/src/pages/Streak.jsx +++ b/src/pages/Streak.jsx @@ -2,19 +2,45 @@ import { useUser } from '@contexts/UserContext'; import FireIcon from '@mui/icons-material/Whatshot'; import { Box, Typography } from '@mui/material'; import '@styles/StreakPage.css'; -import { useEffect } from 'react'; +import { getChicagoDate } from '@utils/streakUtils'; +import { useMemo } from 'react'; import Calendar from 'react-calendar'; import 'react-calendar/dist/Calendar.css'; const Streak = () => { - const { streakCount, completedDays, checkDailyProgress } = useUser(); + const { user } = useUser(); - const today = new Date().toISOString().split('T')[0]; + const streakCount = user.streak?.count || 0; + const completedDays = user.streak?.completedDays || {}; + const today = getChicagoDate(); - useEffect(() => { - //check for daily progress based on goals or tasks whenever component mounts or updates - checkDailyProgress(); - }, [checkDailyProgress]); + // Cache the completed dates + const completedDatesSet = useMemo( + () => new Set(Object.keys(completedDays).filter((date) => completedDays[date] > 0)), + [completedDays], + ); + + // Function to get the tile icon for a date + const getTileIcon = (date) => { + const formattedDate = date.toISOString().split('T')[0]; + const isCompleted = completedDatesSet.has(formattedDate); + const isPastOrToday = formattedDate <= today; + + if (isPastOrToday) { + return ( + + + + ); + } + return null; + }; return ( @@ -30,25 +56,7 @@ const Streak = () => { - { - const formattedDate = date.toISOString().split('T')[0]; - const isCompleted = completedDays.includes(formattedDate); - const isPastOrToday = formattedDate <= today; - - return view === 'month' && isPastOrToday ? ( - - - - ) : null; - }} - /> + (view === 'month' ? getTileIcon(date) : null)} /> ); diff --git a/src/utils/firebase/createUserProfile.js b/src/utils/firebase/createUserProfile.js index 77dce91..85aa6ec 100644 --- a/src/utils/firebase/createUserProfile.js +++ b/src/utils/firebase/createUserProfile.js @@ -35,10 +35,7 @@ export const createFirstUserProfile = async (user) => { profilePic: photoURL || '', name: displayName || '', goals: [], - streakData: { - completedDays: [], // Array to store dates when tasks were completed - streakCount: 0, // Initial streak count - }, + streak: [], }; try { diff --git a/src/utils/firebase/streakUtils.js b/src/utils/firebase/streakUtils.js deleted file mode 100644 index cfa538d..0000000 --- a/src/utils/firebase/streakUtils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { db } from '@utils/firebaseConfig'; -import { doc, getDoc, updateDoc } from 'firebase/firestore'; - -/** - * Fetch streak data from Firestore by user ID. - * @param {string} uid - The user's unique identifier. - * @returns {object} - Streak data with `completedDays` and `streakCount`. - */ -export const getStreakData = async (uid) => { - try { - const userRef = doc(db, 'users', uid); - const userSnapshot = await getDoc(userRef); - - if (userSnapshot.exists()) { - const data = userSnapshot.data(); - return data.streakData || { completedDays: [], streakCount: 0 }; - } else { - console.error(`No streak data found for user ${uid}`); - return { completedDays: [], streakCount: 0 }; - } - } catch (error) { - console.error('Error fetching streak data:', error); - return { completedDays: [], streakCount: 0 }; - } -}; - -/** - * Update streak data in Firestore for the specified user. - * @param {string} uid - The user's unique identifier. - * @param {Array} completedDays - Array of dates when goals were completed. - * @param {number} streakCount - Current streak count. - * @returns {boolean} - Success status of the update. - */ -export const updateStreakData = async (uid, completedDays, streakCount) => { - try { - const userDocRef = doc(db, 'users', uid); - await updateDoc(userDocRef, { - 'streakData.completedDays': completedDays, - 'streakData.streakCount': streakCount, - }); - console.info('Streak data updated successfully.'); - return true; - } catch (error) { - console.error('Error updating streak data:', error); - return false; - } -}; diff --git a/src/utils/streakUtils.js b/src/utils/streakUtils.js new file mode 100644 index 0000000..7b46f71 --- /dev/null +++ b/src/utils/streakUtils.js @@ -0,0 +1,32 @@ +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]; +}; + +// Function to update the streak count and completed days for a user +export const updateStreakDays = (user, countChange) => { + const currentDate = getChicagoDate(); + + const completedDays = user.streak?.completedDays || {}; // {date: count, ...} + const streakCount = user.streak?.count || 0; // Initial streak count if not set yet + + // Update the completed days count + const currentCount = completedDays[currentDate] || 0; + completedDays[currentDate] = Math.max(0, currentCount + countChange); + + // Check if the streak count needs to be updated + let newStreakCount = streakCount; + if (currentCount === 0 && countChange > 0) { + // 0 -> positive, streak count increases + newStreakCount++; + } else if (currentCount > 0 && completedDays[currentDate] === 0) { + // positive -> 0, streak count decreases + newStreakCount = Math.max(0, newStreakCount - 1); + } + + return { + completedDays, + count: newStreakCount, + }; +}; diff --git a/src/utils/taskCompletion.js b/src/utils/taskCompletion.js deleted file mode 100644 index 395e1fb..0000000 --- a/src/utils/taskCompletion.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Get unique dates on which tasks have been completed - * @param {Array} goals - List of goal objects, each containing microgoals and tasks - * @returns {Array} - Unique dates when tasks were marked as completed - */ -export const getCompletedDates = (goals) => { - const completedDates = new Set(); - - // Check if goals is an array and iterate only if it exists - if (Array.isArray(goals)) { - goals.forEach((goal) => { - // Ensure microgoals is defined and an array - if (Array.isArray(goal.microgoals)) { - goal.microgoals.forEach((microGoal) => { - // Ensure tasks is defined and an array - if (Array.isArray(microGoal.tasks)) { - microGoal.tasks.forEach((task) => { - // Check for both task completion and valid completion date - if (task.completed && task.completionDate) { - completedDates.add(task.completionDate); - } - }); - } - }); - } - }); - } - - return Array.from(completedDates); -}; From bdff722902b6bba46f560f40c6ef7e72d20d0bcf Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:02:48 -0600 Subject: [PATCH 8/8] doc(README): update streak related. --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b807c99..825d673 100644 --- a/README.md +++ b/README.md @@ -109,10 +109,11 @@ The `updateProfile` function accepts an object with updated user fields and sync The `useGoalsUpdater` custom hook provides functions to manage user goals, microgoals, and tasks by: -1. updating the user's profile goals by adding/deleting items (goal, microgoal, task), -2. deleting items (goal, microgoal, task), -3. toggling completion status for tasks, and -4. expanding or collapsing goals and microgoals. +1. updating the user's profile, including both goals and (optionally) the streak, +2. adding items (goal, microgoal, task), +3. deleting items (goal, microgoal, task), +4. toggling completion status for tasks with streak updates, and +5. expanding or collapsing goals and microgoals. This hook enables efficient management of goal-related actions across components, reducing repetitive code and ensuring consistent updates. @@ -124,7 +125,7 @@ This hook enables efficient management of goal-related actions across components #### Adding items -- **addGoal**: Adds a new goal with an optional name. +- **addGoal**: Adds a new goal with a specified name. ```jsx addGoal("New Goal Name"); @@ -164,7 +165,7 @@ deleteTask(goalIndex, microGoalIndex, taskIndex); #### Toggle Task Completion -- **toggleTaskCompletion**: Toggles the completion status of a task. +- **toggleTaskCompletion**: Toggles the completion status of a task and updates the streak accordingly. ```jsx toggleTaskCompletion(goalIndex, microGoalIndex, taskIndex);