Skip to content
Open
59 changes: 51 additions & 8 deletions src/context/AuthContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,13 @@ export const AuthProvider = ({ children }) => {
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,
Expand All @@ -152,23 +158,59 @@ export const AuthProvider = ({ children }) => {
onboardingStatus: "incomplete",
privateRepoSyncEnabled: requestRepoScope,
city: "",
streak: 0,
streak: 1, // Start streak
longestStreak: 0,
githubStreak: 0,
lastLogin: new Date().toISOString(),
createdAt: new Date().toISOString(),
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 });
}
Expand Down Expand Up @@ -357,7 +399,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;
Expand All @@ -382,6 +424,7 @@ export const AuthProvider = ({ children }) => {
const newGitRankPoints = ghStats.gitRankPoints;
const newTotalPoints = newGitRankPoints + currentReferralPoints + currentCodingVersePoints + currentStreakPoints;

// Retained the Atomic Batch Writes (Issue #193)
// Phase 2: Issue Atomic Batch Write
const batch = writeBatch(db);

Expand Down Expand Up @@ -409,7 +452,7 @@ export const AuthProvider = ({ children }) => {
};

return (
<AuthContext.Provider value={{ user, userData, loading, isOnboarding, login, logout, fetchGitHubStats, syncGitHubData, ghAccessToken, setUserData }}>
<AuthContext.Provider value={{ user, userData, loading, isOnboarding, login, logout, fetchGitHubStats, syncGitHubData, ghAccessToken, setUserData }}>
{children}
</AuthContext.Provider>
);
Expand Down
7 changes: 1 addition & 6 deletions src/pages/RankHer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -40,10 +36,9 @@ export const RankHer = () => {
);

return () => unsubscribe();
}, [user]);
}, []);

const renderBody = () => {

if (womenUsers === null) {
return (
<div className="flex items-center justify-center py-12 text-slate-400">
Expand Down