From 944310454c88968b5c899cac75963ce1ba7864e0 Mon Sep 17 00:00:00 2001 From: Nitish Date: Wed, 3 Jun 2026 13:37:46 +0530 Subject: [PATCH 1/4] fix: implement timezone-agnostic streak calculation using strict UTC boundaries (#191) --- src/context/AuthContext.jsx | 122 ++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 39 deletions(-) diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index f28edcd..42b799b 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -9,7 +9,7 @@ import { getDoc, setDoc, onSnapshot, - runTransaction + writeBatch } from "firebase/firestore"; import axios from "axios"; import { auth, db, signInWithGitHub, signOutUser } from "../lib/firebase"; @@ -23,7 +23,7 @@ export const AuthProvider = ({ children }) => { const [userData, setUserData] = useState(null); const [loading, setLoading] = useState(true); const [isOnboarding, setIsOnboarding] = useState(false); -// GitHub OAuth access token persisted in sessionStorage to survive page refreshes + // GitHub OAuth access token persisted in sessionStorage to survive page refreshes const [ghAccessToken, setGhAccessToken] = useState(() => { return sessionStorage.getItem("gh_access_token") || null; }); @@ -86,16 +86,21 @@ export const AuthProvider = ({ children }) => { const githubId = additionalInfo?.profile?.id || null; const avatar = additionalInfo?.profile?.avatar_url || authUser.photoURL || ""; -// Save the token to sessionStorage and state to keep user authenticated across refreshes + // Save the token to sessionStorage and state to keep user authenticated across refreshes sessionStorage.setItem("gh_access_token", accessToken); sessionStorage.setItem(`gh_token_${authUser.uid}`, accessToken); setGhAccessToken(accessToken); - setGhAccessToken(accessToken); const userDocRef = doc(db, "users", authUser.uid); const docSnap = await getDoc(userDocRef); + // Issue #191: Strict Timezone-Agnostic UTC Streak Calculation + const today = new Date(); + const todayUTCStr = today.toISOString().split('T')[0]; // Format: YYYY-MM-DD + const todayUTC = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); + if (!docSnap.exists()) { + // First login ever const skeletalUser = { uid: authUser.uid, githubUsername, @@ -106,21 +111,57 @@ export const AuthProvider = ({ children }) => { onboardingStatus: "incomplete", privateRepoSyncEnabled: requestRepoScope, city: "", - streak: 0, - lastLogin: new Date().toISOString(), - createdAt: new Date().toISOString(), + streak: 1, // Start streak + lastLogin: today.toISOString(), + createdAt: today.toISOString(), points: { gitRankPoints: 0, codingVersePoints: 0, - streakPoints: 0, + streakPoints: 10, // Base points for 1st day streak referralPoints: 0, - totalPoints: 0 + totalPoints: 10 } }; await setDoc(userDocRef, skeletalUser); } else { + // Existing user: Calculate streak safely + const existingData = docSnap.data(); + let newStreak = existingData.streak || 0; + let newStreakPoints = existingData.points?.streakPoints || 0; + let newTotalPoints = existingData.points?.totalPoints || 0; + + const lastLoginDate = existingData.lastLogin ? new Date(existingData.lastLogin) : null; + + if (lastLoginDate) { + const lastLoginUTCStr = lastLoginDate.toISOString().split('T')[0]; + + // Only process streak logic if it's a completely new UTC day + if (todayUTCStr !== lastLoginUTCStr) { + const lastUTC = Date.UTC(lastLoginDate.getUTCFullYear(), lastLoginDate.getUTCMonth(), lastLoginDate.getUTCDate()); + const diffDays = Math.floor((todayUTC - lastUTC) / (1000 * 60 * 60 * 24)); + + if (diffDays === 1) { + newStreak += 1; // Perfect continuation + } else if (diffDays > 1) { + newStreak = 1; // Streak broken, restart + } + + // Award 10 points for the new active day + newStreakPoints += 10; + newTotalPoints += 10; + } + } else { + // Fallback if lastLogin was somehow missing + newStreak = 1; + newStreakPoints += 10; + newTotalPoints += 10; + } + await setDoc(userDocRef, { - lastLogin: new Date().toISOString(), + lastLogin: today.toISOString(), + streak: newStreak, + "points.streakPoints": newStreakPoints, + "points.totalPoints": newTotalPoints, ...(requestRepoScope && { privateRepoSyncEnabled: true }) }, { merge: true }); } @@ -240,7 +281,7 @@ export const AuthProvider = ({ children }) => { if (userData.lastSync) { const lastSyncTime = new Date(userData.lastSync).getTime(); - const cooldownMs = 5 * 60 * 1000; + const cooldownMs = 5 * 60 * 1000; // 5 minutes if (Date.now() - lastSyncTime < cooldownMs) { console.log("Background GitHub sync skipped: Cooldown active."); return; @@ -251,42 +292,45 @@ export const AuthProvider = ({ children }) => { const ghStats = await fetchGitHubStats(user.uid, userData.githubUsername); const userRef = doc(db, "users", user.uid); - await runTransaction(db, async (transaction) => { - const userDoc = await transaction.get(userRef); - if (!userDoc.exists()) { - throw new Error("User document does not exist in Firestore!"); - } + const userDoc = await getDoc(userRef); + if (!userDoc.exists()) { + throw new Error("User document does not exist in Firestore!"); + } - const liveData = userDoc.data(); - const currentReferralPoints = liveData.points?.referralPoints || 0; - const currentCodingVersePoints = liveData.points?.codingVersePoints || 0; - const currentStreakPoints = liveData.points?.streakPoints || 0; - - const newGitRankPoints = ghStats.gitRankPoints; - const newTotalPoints = newGitRankPoints + currentReferralPoints + currentCodingVersePoints + currentStreakPoints; - - transaction.update(userRef, { - "githubStats.commits": ghStats.commits, - "githubStats.prs": ghStats.prs, - "githubStats.reviews": ghStats.reviews, - "githubStats.repos": ghStats.publicRepos, - "githubStats.stars": ghStats.stars, - "githubStats.followers": ghStats.followers, - "githubStats.primaryLanguage": ghStats.primaryLanguage, - "points.gitRankPoints": newGitRankPoints, - "points.totalPoints": newTotalPoints, - "lastSync": new Date().toISOString() - }); + const liveData = userDoc.data(); + const currentReferralPoints = liveData.points?.referralPoints || 0; + const currentCodingVersePoints = liveData.points?.codingVersePoints || 0; + const currentStreakPoints = liveData.points?.streakPoints || 0; + + const newGitRankPoints = ghStats.gitRankPoints; + const newTotalPoints = newGitRankPoints + currentReferralPoints + currentCodingVersePoints + currentStreakPoints; + + // Retained the Atomic Batch Writes (Issue #193) + const batch = writeBatch(db); + + batch.update(userRef, { + "githubStats.commits": ghStats.commits, + "githubStats.prs": ghStats.prs, + "githubStats.reviews": ghStats.reviews, + "githubStats.repos": ghStats.publicRepos, + "githubStats.stars": ghStats.stars, + "githubStats.followers": ghStats.followers, + "githubStats.primaryLanguage": ghStats.primaryLanguage, + "points.gitRankPoints": newGitRankPoints, + "points.totalPoints": newTotalPoints, + "lastSync": new Date().toISOString() }); - console.log("Background GitHub sync completed successfully."); + + await batch.commit(); + console.log("Background GitHub sync completed successfully via atomic batch."); } catch (error) { console.error("Background GitHub sync failed:", error); } }; return ( - + {children} ); -}; +}; \ No newline at end of file From 48d8273a7ebb704b476c6663d3803c963df5e901 Mon Sep 17 00:00:00 2001 From: Nitish Date: Thu, 4 Jun 2026 15:32:59 +0530 Subject: [PATCH 2/4] fix: resolve merge conflicts in AuthContext skeletal user block --- src/context/AuthContext.jsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index 9cb03be..0c147e8 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -158,17 +158,11 @@ export const AuthProvider = ({ children }) => { onboardingStatus: "incomplete", privateRepoSyncEnabled: requestRepoScope, city: "", -<<<<<<< HEAD - streak: 1, // Start streak - lastLogin: today.toISOString(), - createdAt: today.toISOString(), -======= - streak: 0, - longestStreak: 0, - githubStreak: 0, - lastLogin: new Date().toISOString(), - createdAt: new Date().toISOString(), ->>>>>>> main + streak: 1, // Start streak + longestStreak: 0, + githubStreak: 0, + lastLogin: today.toISOString(), + createdAt: today.toISOString(), points: { gitRankPoints: 0, codingVersePoints: 0, From aeb1901ec5f98f9e127d5e660fc1ae968dc02617 Mon Sep 17 00:00:00 2001 From: Nitish Date: Thu, 4 Jun 2026 15:44:24 +0530 Subject: [PATCH 3/4] chore: trigger CI pipeline on clean local code From 40e04b1a9fedc4ef38e942dd6c37039effd219c5 Mon Sep 17 00:00:00 2001 From: Nitish Date: Thu, 4 Jun 2026 16:22:17 +0530 Subject: [PATCH 4/4] fix: resolve final merge conflicts and clean all linting errors --- src/pages/RankHer.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/RankHer.jsx b/src/pages/RankHer.jsx index 1ad9150..92b8f6d 100644 --- a/src/pages/RankHer.jsx +++ b/src/pages/RankHer.jsx @@ -2,20 +2,16 @@ import React, { useEffect, useState } from "react"; import { Sparkles, Quote, Star, Loader2 } from "lucide-react"; import { collection, query, where, orderBy, limit, onSnapshot } from "firebase/firestore"; import { db } from "../lib/firebase"; -import { useAuth } from "../context/AuthContext"; import Card from "../components/ui/Card"; import SectionHeader from "../components/ui/SectionHeader"; export const RankHer = () => { - const { user } = useAuth(); - // null = subscription has not yet returned data (loading) // [] = subscription returned, zero qualifying users // [...] = real users ranked by totalPoints const [womenUsers, setWomenUsers] = useState(null); useEffect(() => { - const q = query( collection(db, "users"), where("onboardingStatus", "==", "complete"), @@ -40,10 +36,9 @@ export const RankHer = () => { ); return () => unsubscribe(); - }, [user]); + }, []); const renderBody = () => { - if (womenUsers === null) { return (