11<script setup lang="ts">
2- import {ref , computed , onMounted , onUnmounted } from ' vue' ;
2+ import {ref , computed , onMounted , onUnmounted , watch } from ' vue' ;
33import {storeToRefs } from ' pinia' ;
44import ModalOverlay from ' components/ModalOverlay.vue' ;
55
@@ -9,6 +9,11 @@ import {BADGE_IMAGE_MAP} from '../widgets/badge_images';
99import {DEMO_USERS , DEMO_COMMUNITY_EDITS_WEEK , DEMO_COMMUNITY_EDITS_MONTH } from ' ../data/demo-users' ;
1010import pyrIcon from ' ../../static/badges/pyr/pyr-icon.png' ;
1111
12+ // ── Props ─────────────────────────────────────────────────────────────────────
13+ const props = defineProps <{
14+ viewUserId? : string | null ;
15+ }>();
16+
1217// ── Stores ────────────────────────────────────────────────────────────────────
1318const {sessions} = storeToRefs (useLoginStore ());
1419const statsStore = useUserStatsStore ();
@@ -18,13 +23,58 @@ const {prefs} = storeToRefs(prefsStore);
1823const historyStore = useCellHistoryStore ();
1924const {cells : cellHistory} = storeToRefs (historyStore );
2025
21- // Refresh stats from Supabase when profile opens
2226const backendStore = useProofreadingBackendStore ();
23- backendStore .loadUserStats ();
27+
28+ // ── Viewing another user's profile ────────────────────────────────────────────
29+ const viewingOtherUser = computed (() => !! props .viewUserId && props .viewUserId !== backendStore .userId );
30+ const otherUserProfile = ref <any >(null );
31+
32+ async function loadOtherUser() {
33+ if (viewingOtherUser .value && props .viewUserId ) {
34+ otherUserProfile .value = await backendStore .loadUserProfile (props .viewUserId );
35+ } else {
36+ otherUserProfile .value = null ;
37+ backendStore .loadUserStats ();
38+ }
39+ }
40+ loadOtherUser ();
41+
42+ // Profile display name & email — works for both self and other users
43+ const profileName = computed (() => {
44+ if (viewingOtherUser .value && otherUserProfile .value ) return otherUserProfile .value .display_name || ' Anonymous' ;
45+ return backendStore .userName || sessions .value ?.[0 ]?.name || ' Researcher' ;
46+ });
47+ const profileEmail = computed (() => {
48+ if (viewingOtherUser .value && otherUserProfile .value ) return otherUserProfile .value .middleauth_email || ' ' ;
49+ return backendStore .userEmail || sessions .value ?.[0 ]?.email || ' ' ;
50+ });
51+ const profileFlag = computed (() => {
52+ if (viewingOtherUser .value && otherUserProfile .value ) return otherUserProfile .value .flag || ' ' ;
53+ return prefs .value .flag || ' ' ;
54+ });
55+ const profileStats = computed (() => {
56+ if (viewingOtherUser .value && otherUserProfile .value ) {
57+ const u = otherUserProfile .value ;
58+ return {
59+ editsAllTime: u .total_edits || 0 , mergesAllTime: u .total_merges || 0 , splitsAllTime: u .total_splits || 0 ,
60+ cellsSubmitted: u .cells_completed || 0 , currentStreak: u .current_streak || 0 , longestStreak: u .longest_streak || 0 ,
61+ // Weekly/monthly/today not available for other users — show 0
62+ editsThisWeek: 0 , mergesThisWeek: 0 , splitsThisWeek: 0 ,
63+ editsThisMonth: 0 , mergesThisMonth: 0 , splitsThisMonth: 0 ,
64+ editsToday: 0 , mergesToday: 0 , splitsToday: 0 ,
65+ };
66+ }
67+ return stats .value ;
68+ });
69+ const profileSpecialBadges = computed (() => {
70+ if (viewingOtherUser .value && otherUserProfile .value ) return otherUserProfile .value .specialBadges || [];
71+ return profileSpecialBadges ;
72+ });
2473
2574// Player assists — help requests resolved by current user
2675const helpStore = useHelpRequestStore ();
2776const playerAssists = computed (() => {
77+ if (viewingOtherUser .value ) return 0 ;
2878 const name = backendStore .userName || backendStore .userEmail ?.split (' @' )[0 ];
2979 if (! name ) return 0 ;
3080 return helpStore .requests .filter (r => r .resolved && r .resolvedByName === name ).length ;
@@ -126,9 +176,10 @@ function getBadgeUrl(imageKey: string): string {
126176
127177/** Get the player's current count for a given track. */
128178function statForTrack(track : BadgeTrack ): number {
179+ const s = profileStats .value ;
129180 return track === ' building'
130- ? (stats . value .editsAllTime ?? 0 )
131- : (stats . value .cellsSubmitted ?? 0 );
181+ ? (s .editsAllTime ?? 0 )
182+ : (s .cellsSubmitted ?? 0 );
132183}
133184
134185function isBadgeEarned(badge : BadgeDefinition ): boolean {
@@ -181,7 +232,7 @@ const favoriteBadge = computed<BadgeDefinition | null>(() => {
181232/** Check if the favorite is a special badge (not a regular track badge). */
182233const favoriteSpecialBadge = computed (() => {
183234 if (! favoriteBadgeSlug .value || favoriteBadge .value ) return null ;
184- return backendStore . mySpecialBadges .find (
235+ return profileSpecialBadges .find (
185236 (a : any ) => (a .badge ?.slug || ` special-${a .id } ` ) === favoriteBadgeSlug .value
186237 ) || null ;
187238});
@@ -231,10 +282,10 @@ function nextForTrack(badges: BadgeDefinition[], current: number) {
231282}
232283
233284const nextBuildingAchievement = computed (() =>
234- nextForTrack (BUILDING_BADGES , stats .value .editsAllTime ?? 0 )
285+ nextForTrack (BUILDING_BADGES , profileStats .value .editsAllTime ?? 0 )
235286);
236287const nextExplorationAchievement = computed (() =>
237- nextForTrack (EXPLORATION_BADGES , stats .value .cellsSubmitted ?? 0 )
288+ nextForTrack (EXPLORATION_BADGES , profileStats .value .cellsSubmitted ?? 0 )
238289);
239290
240291// ── Current dataset helper (for filtering cells) ────────────────────────────
@@ -377,18 +428,18 @@ const emit = defineEmits({hide: null, 'open-settings': null});
377428 <div class =" nge-profile-col nge-profile-col--left" >
378429
379430 <!-- Header -->
380- <div class =" nge-profile-header" v-if =" sessions.length > 0" >
431+ <div class =" nge-profile-header" v-if =" sessions.length > 0 || viewingOtherUser " >
381432 <div class =" nge-profile-name-row" >
382433
383- <!-- Flag with inline picker -->
384- <div id =" nge-profile-flag-wrap" class =" nge-profile-flag-wrap" @click.stop >
434+ <!-- Flag (editable only for own profile) -->
435+ <div v-if = " !viewingOtherUser " id =" nge-profile-flag-wrap" class =" nge-profile-flag-wrap" @click.stop >
385436 <button
386437 class =" nge-profile-flag"
387438 :class =" { 'nge-profile-flag--active': showFlagPicker }"
388439 @click =" showFlagPicker = !showFlagPicker"
389440 title =" Click to change flag"
390441 >
391- <img v-if =" flagImgUrl(prefs.flag || '' )" :src =" flagImgUrl(prefs.flag )" class =" nge-flag-img" />
442+ <img v-if =" flagImgUrl(profileFlag )" :src =" flagImgUrl(profileFlag )" class =" nge-flag-img" />
392443 <img v-else :src =" pyrIcon" class =" nge-flag-img nge-pyr-icon" />
393444 </button >
394445 <Transition name =" nge-flag-picker" >
@@ -402,30 +453,35 @@ const emit = defineEmits({hide: null, 'open-settings': null});
402453 </div >
403454 </Transition >
404455 </div >
456+ <div v-else class =" nge-profile-flag-wrap" >
457+ <img v-if =" flagImgUrl(profileFlag)" :src =" flagImgUrl(profileFlag)" class =" nge-flag-img" style =" width :28px ;height :20px ;" />
458+ </div >
405459
406- <div class =" nge-profile-name" >{{ sessions[0].name || sessions[0].email?.split('@')[0] || 'Explorer' }}</div >
460+ <div class =" nge-profile-name" >{{ profileName }}</div >
407461
408- <button class =" nge-profile-edit-btn"
462+ <button v-if = " !viewingOtherUser " class =" nge-profile-edit-btn"
409463 @click =" emit('open-settings')"
410464 title =" Edit Profile — set bio, flag, and more" >⚙</button >
411465 </div >
412466
413- <div class =" nge-profile-email" >{{ sessions[0].email }}</div >
467+ <div class =" nge-profile-email" >{{ profileEmail }}</div >
414468
415- <div class =" nge-profile-bio" v-if =" prefs.bio" >{{ prefs.bio }}</div >
416- <button v-else class =" nge-profile-bio-add" @click =" emit('open-settings')" >
417- + Add a bio
418- </button >
469+ <template v-if =" ! viewingOtherUser " >
470+ <div class =" nge-profile-bio" v-if =" prefs.bio" >{{ prefs.bio }}</div >
471+ <button v-else class =" nge-profile-bio-add" @click =" emit('open-settings')" >
472+ + Add a bio
473+ </button >
474+ </template >
419475 </div >
420476
421477 <!-- Edits stats -->
422478 <div class =" nge-profile-section nge-profile-section--edits" >
423479 <div class =" nge-profile-section-label" >▌ Edits</div >
424480 <div class =" nge-profile-stat-row" >
425481 <div class =" nge-profile-stat-col" v-for =" (col, i) in [
426- {label:'Today', val:stats .editsToday, merges:stats .mergesToday, splits:stats .splitsToday},
427- {label:'Past 7d', val:stats .editsThisWeek, merges:stats .mergesThisWeek, splits:stats .splitsThisWeek},
428- {label:'All Time', val:stats .editsAllTime, merges:stats .mergesAllTime, splits:stats .splitsAllTime},
482+ {label:'Today', val:profileStats .editsToday, merges:profileStats .mergesToday, splits:profileStats .splitsToday},
483+ {label:'Past 7d', val:profileStats .editsThisWeek, merges:profileStats .mergesThisWeek, splits:profileStats .splitsThisWeek},
484+ {label:'All Time', val:profileStats .editsAllTime, merges:profileStats .mergesAllTime, splits:profileStats .splitsAllTime},
429485 ]" :key =" i" >
430486 <div class =" nge-profile-stat-label" >{{ col.label }}</div >
431487 <div class =" nge-profile-stat-val" >{{ col.val.toLocaleString() }}</div >
@@ -457,7 +513,7 @@ const emit = defineEmits({hide: null, 'open-settings': null});
457513 </div >
458514
459515 <!-- Cells stats -->
460- <div class =" nge-profile-section nge-profile-section--cells" >
516+ <div v-if = " !viewingOtherUser " class =" nge-profile-section nge-profile-section--cells" >
461517 <div class =" nge-profile-section-label" >▌ Cells</div >
462518 <div class =" nge-profile-stat-row" >
463519 <div class =" nge-profile-stat-col" >
@@ -652,13 +708,13 @@ const emit = defineEmits({hide: null, 'open-settings': null});
652708 </div >
653709
654710 <!-- Special Awards (admin-awarded badges) -->
655- <template v-if =" backendStore . mySpecialBadges .length > 0 " >
711+ <template v-if =" profileSpecialBadges .length > 0 " >
656712 <div class =" nge-profile-badges-divider" ></div >
657713 <div class =" nge-profile-section nge-profile-section--badges nge-profile-section--special" >
658714 <div class =" nge-profile-section-label" >★ Special Awards</div >
659715 <div class =" nge-profile-badges-grid" >
660716 <div
661- v-for =" award in backendStore.mySpecialBadges "
717+ v-for =" award in profileSpecialBadges "
662718 :key =" award.id"
663719 class =" nge-profile-badge"
664720 :class =" { 'nge-profile-badge--selected': selectedSpecialBadge?.id === award.id }"
@@ -774,15 +830,15 @@ const emit = defineEmits({hide: null, 'open-settings': null});
774830 <!-- Streak + Activity Chart -->
775831 <div class =" nge-profile-section nge-profile-section--streak" >
776832 <div class =" nge-profile-section-label nge-profile-section-label--amber" >▌ Streak</div >
777- <div class =" nge-profile-streak-row" v-if =" stats .currentStreak > 0 || stats .longestStreak > 0" >
833+ <div class =" nge-profile-streak-row" v-if =" profileStats .currentStreak > 0 || profileStats .longestStreak > 0" >
778834 <div class =" nge-profile-streak-current" >
779835 <span class =" nge-profile-streak-flame" >🔥</span >
780- <span class =" nge-profile-streak-count" >{{ stats .currentStreak }}</span >
781- <span class =" nge-profile-streak-unit" >day{{ stats .currentStreak === 1 ? '' : 's' }} current</span >
836+ <span class =" nge-profile-streak-count" >{{ profileStats .currentStreak }}</span >
837+ <span class =" nge-profile-streak-unit" >day{{ profileStats .currentStreak === 1 ? '' : 's' }} current</span >
782838 </div >
783- <div class =" nge-profile-streak-best" v-if =" stats .longestStreak > 0" >
839+ <div class =" nge-profile-streak-best" v-if =" profileStats .longestStreak > 0" >
784840 <span class =" nge-profile-streak-best-label" >Best</span >
785- <span class =" nge-profile-streak-best-val" >{{ stats .longestStreak }}d</span >
841+ <span class =" nge-profile-streak-best-val" >{{ profileStats .longestStreak }}d</span >
786842 </div >
787843 </div >
788844
@@ -973,11 +1029,11 @@ const emit = defineEmits({hide: null, 'open-settings': null});
9731029 </div >
9741030
9751031 <!-- Special Awards -->
976- <div v-if =" backendStore.mySpecialBadges .length > 0" class =" nge-trophy-track" >
1032+ <div v-if =" profileSpecialBadges .length > 0" class =" nge-trophy-track" >
9771033 <div class =" nge-trophy-track-label" >★ Special Awards</div >
9781034 <div class =" nge-trophy-grid" >
9791035 <div
980- v-for =" award in backendStore.mySpecialBadges "
1036+ v-for =" award in profileSpecialBadges "
9811037 :key =" award.id"
9821038 class =" nge-trophy-badge"
9831039 :class =" { 'nge-trophy-badge--selected': selectedSpecialBadge?.id === award.id }"
0 commit comments