From def1840a650d5440a645a37d300653c08fa64259 Mon Sep 17 00:00:00 2001 From: shivani11jadhav Date: Fri, 12 Jun 2026 18:11:11 +0530 Subject: [PATCH] fix(profile): resolve real-time state desync between profile update form and DevCard using onSnapshot --- package-lock.json | 17 ++++++ src/components/profile/DevCard.tsx | 90 ++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4fb7598..343f1db3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "jest-environment-jsdom": "^30.4.1", "postcss": "^8.5.10", "postcss-nesting": "^13.0.2", + "prettier": "^3.2.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", @@ -19444,6 +19445,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", diff --git a/src/components/profile/DevCard.tsx b/src/components/profile/DevCard.tsx index 8d288af0..19141dde 100644 --- a/src/components/profile/DevCard.tsx +++ b/src/components/profile/DevCard.tsx @@ -11,7 +11,7 @@ import { } from 'lucide-react'; import { calculateLevel } from '@/lib/points'; import { copyToClipboard } from '@/lib/clipboard'; -import { collection, query, where, getCountFromServer } from 'firebase/firestore'; +import { collection, query, where, getCountFromServer, doc, onSnapshot } from 'firebase/firestore'; import { db } from '@/lib/firebase'; import { useNotificationActions } from '@/stores/ui-store'; import styles from './DevCard.module.css'; @@ -117,24 +117,56 @@ export default function DevCard({ user }: { user: any }) { const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); const { showSuccess, showError } = useNotificationActions(); + const [realTimeUser, setRealTimeUser] = useState(user); + + useEffect(() => { + setRealTimeUser(user); + }, [user]); + + useEffect(() => { + if (!user?.uid) return; + + const collectionName = user.role === 'admin' ? 'admins' : 'members'; + const docId = user.role === 'admin' ? user.email?.toLowerCase() : user.uid; + + if (!docId) return; + + const docRef = doc(db, collectionName, docId); + const unsubscribe = onSnapshot(docRef, (docSnap) => { + if (docSnap.exists()) { + const data = docSnap.data(); + setRealTimeUser((prev: any) => ({ + ...prev, + ...data, + uid: user.uid, + role: user.role, + })); + } + }, (error) => { + console.error("Error listening to profile changes in DevCard:", error); + }); + + return () => unsubscribe(); + }, [user?.uid, user?.role, user?.email]); + useEffect(() => { const fetch = async () => { - if (!user?.points) { setRankLoading(false); return; } + if (!realTimeUser?.points) { setRankLoading(false); return; } try { const snap = await getCountFromServer( - query(collection(db, 'leaderboard'), where('points', '>', user.points)) + query(collection(db, 'leaderboard'), where('points', '>', realTimeUser.points)) ); setRank(snap.data().count + 1); } catch { /* silent */ } finally { setRankLoading(false); } }; fetch(); - }, [user?.points]); + }, [realTimeUser?.points]); useEffect(() => { setShowSkeleton(true); const timer = setTimeout(() => setShowSkeleton(false), 650); return () => clearTimeout(timer); - }, [user?.uid]); + }, [realTimeUser?.uid]); useEffect(() => { const t = setTimeout(() => setLangMounted(true), 500); @@ -143,28 +175,28 @@ export default function DevCard({ user }: { user: any }) { useEffect(() => { setAvatarLoadFailed(false); - }, [user?.photoURL]); + }, [realTimeUser?.photoURL]); - const levelInfo = calculateLevel(user?.points ?? 0); + const levelInfo = calculateLevel(realTimeUser?.points ?? 0); const level = levelInfo.currentLevel; const levelColor = resolveLevelColor(level.color); const levelBg = resolveLevelBg(level.bg); - const earnedBadges = (user?.achievements ?? []) + const earnedBadges = (realTimeUser?.achievements ?? []) .filter((id: string) => BADGE_REGISTRY[id]) .map((id: string) => ({ id, ...BADGE_REGISTRY[id] })); const topBadges = earnedBadges.slice(0, 4); const extraCount = Math.max(0, earnedBadges.length - 4); - const topLangs = ((user?.githubStats?.topLanguages ?? []) as { language: string; count: number }[]).slice(0, 4); + const topLangs = ((realTimeUser?.githubStats?.topLanguages ?? []) as { language: string; count: number }[]).slice(0, 4); const totalLang = topLangs.reduce((s, l) => s + l.count, 0); - const animXP = useAnimatedCount(user?.points ?? 0); - const animStreak = useAnimatedCount(user?.streak ?? 0, 900); + const animXP = useAnimatedCount(realTimeUser?.points ?? 0); + const animStreak = useAnimatedCount(realTimeUser?.streak ?? 0, 900); const profileUrl = typeof window !== 'undefined' - ? `${window.location.origin}/u/${user?.uid}` - : `devpath.in/u/${user?.uid}`; + ? `${window.location.origin}/u/${realTimeUser?.uid}` + : `devpath.in/u/${realTimeUser?.uid}`; const waitForCardImages = async (root: HTMLElement) => { const imgs = Array.from(root.querySelectorAll('img')); @@ -224,7 +256,7 @@ export default function DevCard({ user }: { user: any }) { const url = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = url; - a.download = `devcard-${user?.name?.replace(/\s+/g, '-').toLowerCase() ?? 'devcard'}.png`; + a.download = `devcard-${realTimeUser?.name?.replace(/\s+/g, '-').toLowerCase() ?? 'devcard'}.png`; a.click(); showSuccess('DevCard downloaded successfully.'); } catch { @@ -315,10 +347,10 @@ export default function DevCard({ user }: { user: any }) {
- {user?.photoURL && !avatarLoadFailed ? ( + {realTimeUser?.photoURL && !avatarLoadFailed ? ( {user?.name setAvatarLoadFailed(true)} /> ) : ( -
{user?.name?.charAt(0)?.toUpperCase() ?? '?'}
+
{realTimeUser?.name?.charAt(0)?.toUpperCase() ?? '?'}
)} - {user?.name ?? 'Developer'} + {realTimeUser?.name ?? 'Developer'} {level.name} - {(user?.city || user?.state) && ( - {[user.city, user.state].filter(Boolean).join(', ')} + {(realTimeUser?.city || realTimeUser?.state) && ( + {[realTimeUser.city, realTimeUser.state].filter(Boolean).join(', ')} )} - Joined {fmtDate(user?.createdAt)} - {user?.githubStats?.username && {user.githubStats.username}} - {user?.linkedin && ( - + Joined {fmtDate(realTimeUser?.createdAt)} + {realTimeUser?.githubStats?.username && {realTimeUser.githubStats.username}} + {realTimeUser?.linkedin && ( + LinkedIn )} - {user?.instagram && ( - + {realTimeUser?.instagram && ( + Instagram )} @@ -367,8 +399,8 @@ export default function DevCard({ user }: { user: any }) {
{rankLoading ? '—' : rank ? `#${rank}` : '—'}Global Rank
{animStreak}dStreak
{earnedBadges.length}Badges
-
{user?.completedQuizzes?.length ?? 0}Quizzes
-
{user?.githubStats?.connected ? : }
{user?.githubStats?.connected ? fmtPoints(user.githubStats.totalStars ?? user.githubStats.stars ?? 0) : (user?.followers?.length ?? 0)}{user?.githubStats?.connected ? 'GH Stars' : 'Followers'}
+
{realTimeUser?.completedQuizzes?.length ?? 0}Quizzes
+
{realTimeUser?.githubStats?.connected ? : }
{realTimeUser?.githubStats?.connected ? fmtPoints(realTimeUser.githubStats.totalStars ?? realTimeUser.githubStats.stars ?? 0) : (realTimeUser?.followers?.length ?? 0)}{realTimeUser?.githubStats?.connected ? 'GH Stars' : 'Followers'}
{topBadges.length > 0 && (