Skip to content

Commit

Permalink
refactor(steaks): streamline streak and goal updates
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
ZL-Asica committed Nov 5, 2024
1 parent ffb50ed commit dd412c0
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 207 deletions.
5 changes: 4 additions & 1 deletion src/components/common/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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 (
<AppBar position="sticky" sx={{ backgroundColor: 'primary.light', color: '#000' }}>
<Toolbar sx={{ justifyContent: 'space-between', position: 'relative' }}>
Expand Down
64 changes: 2 additions & 62 deletions src/contexts/UserContext.jsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -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 (
<UserContext.Provider
value={{
Expand All @@ -128,14 +73,9 @@ export const UserProvider = ({ children }) => {
handleSignIn,
handleSignOut,
updateProfile,
streakCount,
completedDays,
checkDailyProgress,
}}
>
{!loading ? children : <LoadingCircle />}
</UserContext.Provider>
);
};

export default UserProvider;
90 changes: 53 additions & 37 deletions src/hooks/useGoalsUpdater.js
Original file line number Diff line number Diff line change
@@ -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.`,
);
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
60 changes: 34 additions & 26 deletions src/pages/Streak.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FireIcon
sx={{
fontSize: 16,
color: isCompleted ? 'primary.main' : 'gray',
animation: isCompleted ? 'burn-animation 2s infinite' : 'none',
}}
/>
</Box>
);
}
return null;
};

return (
<Box sx={{ textAlign: 'center', mt: 4 }}>
Expand All @@ -30,25 +56,7 @@ const Streak = () => {
</Typography>

<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
<Calendar
tileContent={({ date, view }) => {
const formattedDate = date.toISOString().split('T')[0];
const isCompleted = completedDays.includes(formattedDate);
const isPastOrToday = formattedDate <= today;

return view === 'month' && isPastOrToday ? (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FireIcon
sx={{
fontSize: 16,
color: isCompleted ? 'primary.main' : 'gray',
animation: isCompleted ? 'burn-animation 2s infinite' : 'none',
}}
/>
</Box>
) : null;
}}
/>
<Calendar tileContent={({ date, view }) => (view === 'month' ? getTileIcon(date) : null)} />
</Box>
</Box>
);
Expand Down
5 changes: 1 addition & 4 deletions src/utils/firebase/createUserProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit dd412c0

Please sign in to comment.