From d4796d5ba814916a6bda51c1d28d77e80e91f6d4 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 11 Sep 2026 14:33:28 +0200 Subject: [PATCH 1/3] feat(achievements): show achievement rewards as XP Follows daily-api retiring achievement points: achievements now grant XP into the same balance quests feed, so the level shown in Game Center and on the Highest level leaderboard already accounts for them. - read Achievement.xp and AchievementSyncResult.xpGained/totalXp - every "pts" / "points" label becomes XP, including the sync modal counter and the achievements snapshot card - drop the Most achievement points leaderboard, now a strict subset of Highest level - the sync analytics event keeps points_gained alongside xp_gained so existing dashboards keep reporting Requires the API change to be deployed first. Co-Authored-By: Claude Opus 5 (1M context) --- .../filters/AchievementTrackerButton.spec.tsx | 4 +-- .../modals/AchievementCompletionModal.tsx | 4 +-- .../modals/AchievementPickerModal.spec.tsx | 2 +- .../modals/AchievementPickerModal.tsx | 2 +- .../modals/AchievementShowcaseModal.tsx | 4 +-- .../achievement/CompareAchievementsModal.tsx | 6 ++--- .../achievement/sortAchievements.spec.ts | 20 +++++++------- .../modals/achievement/sortAchievements.ts | 2 +- .../ProfileWidgets/AchievementSyncModal.tsx | 19 +++++++------- .../AchievementsWidget.spec.tsx | 26 +++++++++---------- .../ProfileWidgets/AchievementsWidget.tsx | 6 ++--- .../achievements/AchievementCard.spec.tsx | 2 +- .../achievements/AchievementCard.tsx | 2 +- .../achievements/AchievementsList.tsx | 4 +-- .../achievements/ProfileAchievements.tsx | 4 +-- .../snapshot/AchievementsSnapshotCard.tsx | 8 +++--- packages/shared/src/graphql/leaderboard.ts | 15 ----------- .../shared/src/graphql/user/achievements.ts | 12 ++++----- .../hooks/profile/useProfileAchievements.ts | 9 +++---- .../features/snapshot/ShareImages.stories.tsx | 2 +- .../snapshot/SnapshotPlacements.stories.tsx | 4 +-- .../UsersLeaderboardStaticProps.spec.ts | 1 - packages/webapp/lib/gameCenter.spec.ts | 14 +++++----- packages/webapp/lib/gameCenter.ts | 8 +++--- packages/webapp/pages/users.tsx | 12 --------- 25 files changed, 81 insertions(+), 111 deletions(-) diff --git a/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx b/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx index 4e3eced3de0..603b84ddfb7 100644 --- a/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx +++ b/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx @@ -99,7 +99,7 @@ const mockTrackedAchievement: UserAchievement = { image: 'https://daily.dev/ach1.png', type: AchievementType.Milestone, criteria: { targetCount: 5 }, - points: 10, + xp: 10, rarity: null, unit: 'steps', }, @@ -123,7 +123,7 @@ const defaultProfileAchievementsHook = { achievements: [mockTrackedAchievement], unlockedCount: 0, totalCount: 1, - totalPoints: 0, + totalXp: 0, isPending: false, isError: false, }; diff --git a/packages/shared/src/components/modals/AchievementCompletionModal.tsx b/packages/shared/src/components/modals/AchievementCompletionModal.tsx index 4c4a092daff..fbfd124a253 100644 --- a/packages/shared/src/components/modals/AchievementCompletionModal.tsx +++ b/packages/shared/src/components/modals/AchievementCompletionModal.tsx @@ -209,7 +209,7 @@ export const AchievementCompletionModal = ({ {unlockedAchievement.achievement.description}
- +{unlockedAchievement.achievement.points} points + +{unlockedAchievement.achievement.xp} XP
@@ -353,7 +353,7 @@ export const AchievementCompletionModal = ({ type={TypographyType.Footnote} color={TypographyColor.Tertiary} > - {userAchievement.achievement.points} pts + {userAchievement.achievement.xp} XP
diff --git a/packages/shared/src/components/modals/AchievementPickerModal.spec.tsx b/packages/shared/src/components/modals/AchievementPickerModal.spec.tsx index 04adbdf7db2..3d1dd5bf140 100644 --- a/packages/shared/src/components/modals/AchievementPickerModal.spec.tsx +++ b/packages/shared/src/components/modals/AchievementPickerModal.spec.tsx @@ -27,7 +27,7 @@ const createLockedAchievement = ( image: 'https://daily.dev/achievement.png', type: AchievementType.Milestone, criteria: { targetCount: 10 }, - points: 50, + xp: 50, rarity: null, unit: null, }, diff --git a/packages/shared/src/components/modals/AchievementPickerModal.tsx b/packages/shared/src/components/modals/AchievementPickerModal.tsx index ddfc5e3e94f..8572996dc72 100644 --- a/packages/shared/src/components/modals/AchievementPickerModal.tsx +++ b/packages/shared/src/components/modals/AchievementPickerModal.tsx @@ -165,7 +165,7 @@ export const AchievementPickerModal = ({ type={TypographyType.Footnote} color={TypographyColor.Tertiary} > - {userAchievement.achievement.points} pts + {userAchievement.achievement.xp} XP
- {userAchievement.achievement.points} pts + {userAchievement.achievement.xp} XP
- {ua.achievement.points} pts + {ua.achievement.xp} XP
({ @@ -22,7 +22,7 @@ const createAchievement = ({ image: 'https://daily.dev/default-achievement.png', type: AchievementType.Milestone, criteria: { targetCount }, - points, + xp, rarity: null, unit: null, }, @@ -33,31 +33,31 @@ const createAchievement = ({ }); describe('sortLockedAchievements', () => { - it('should filter unlocked achievements and sort by ratio, progress, then points', () => { + it('should filter unlocked achievements and sort by ratio, progress, then xp', () => { const achievements: UserAchievement[] = [ createAchievement({ id: 'ratio-high', progress: 8, targetCount: 10, - points: 10, + xp: 10, }), createAchievement({ id: 'ratio-equal-progress-high', progress: 6, targetCount: 10, - points: 5, + xp: 5, }), createAchievement({ - id: 'ratio-equal-progress-low-points-high', + id: 'ratio-equal-progress-low-xp-high', progress: 6, targetCount: 10, - points: 50, + xp: 50, }), createAchievement({ id: 'unlocked', progress: 10, targetCount: 10, - points: 100, + xp: 100, unlockedAt: new Date().toISOString(), }), ]; @@ -66,7 +66,7 @@ describe('sortLockedAchievements', () => { expect(sorted.map((item) => item.achievement.id)).toEqual([ 'ratio-high', - 'ratio-equal-progress-low-points-high', + 'ratio-equal-progress-low-xp-high', 'ratio-equal-progress-high', ]); }); diff --git a/packages/shared/src/components/modals/achievement/sortAchievements.ts b/packages/shared/src/components/modals/achievement/sortAchievements.ts index 430b392f7e8..6dc86589f47 100644 --- a/packages/shared/src/components/modals/achievement/sortAchievements.ts +++ b/packages/shared/src/components/modals/achievement/sortAchievements.ts @@ -26,6 +26,6 @@ export const sortLockedAchievements = ( return b.progress - a.progress; } - return b.achievement.points - a.achievement.points; + return b.achievement.xp - a.achievement.xp; }); }; diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx index 9c1b6480071..e257ec6398f 100644 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx +++ b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx @@ -98,7 +98,7 @@ const AchievementRevealCard = ({ {achievement.achievement.description} - +{achievement.achievement.points} points + +{achievement.achievement.xp} XP ); @@ -152,7 +152,7 @@ export const AchievementSyncModal = ({ return; } - const baseScore = result.totalPoints - result.pointsGained; + const baseScore = result.totalXp - result.xpGained; setScore(baseScore); setCurrentIndex(0); @@ -169,7 +169,7 @@ export const AchievementSyncModal = ({ if (currentIndex >= result.newlyUnlockedAchievements.length) { setIsRevealComplete(true); - if (result.pointsGained > 0) { + if (result.xpGained > 0) { setShowSparkles(true); } return undefined; @@ -182,8 +182,7 @@ export const AchievementSyncModal = ({ const nextTimer = setTimeout(() => { setScore( (value) => - value + - result.newlyUnlockedAchievements[currentIndex].achievement.points, + value + result.newlyUnlockedAchievements[currentIndex].achievement.xp, ); setIsScoreShaking(true); setCurrentIndex((value) => value + 1); @@ -230,7 +229,9 @@ export const AchievementSyncModal = ({ logEvent({ event_name: LogEvent.CompleteSyncAchievements, extra: JSON.stringify({ - points_gained: result.pointsGained, + xp_gained: result.xpGained, + // kept so dashboards built on the points era keep reporting + points_gained: result.xpGained, newly_unlocked: result.newlyUnlockedAchievements.length, }), }); @@ -285,7 +286,7 @@ export const AchievementSyncModal = ({ type={TypographyType.Footnote} color={TypographyColor.Tertiary} > - Achievement points + Achievement XP - {result.pointsGained > 0 - ? `Congrats! +${result.pointsGained} points earned.` + {result.xpGained > 0 + ? `Congrats! +${result.xpGained} XP earned.` : 'No new achievements unlocked this time.'} diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx index f3b4ad2bd77..ac3dc2f6c04 100644 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx +++ b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx @@ -24,13 +24,13 @@ const createUserAchievement = ({ id, name, rarity, - points, + xp, unlockedAt = '2024-01-01T00:00:00.000Z', }: { id: string; name: string; rarity: number | null; - points: number; + xp: number; unlockedAt?: string | null; }): UserAchievement => ({ achievement: { @@ -39,7 +39,7 @@ const createUserAchievement = ({ description: `${name} description`, image: `https://daily.dev/${id}.png`, type: AchievementType.Instant, - points, + xp, rarity, unit: null, }, @@ -84,44 +84,44 @@ describe('AchievementsWidget', () => { id: 'common', name: 'Common', rarity: 20, - points: 10, + xp: 10, }), createUserAchievement({ - id: 'rarity-tie-low-points', + id: 'rarity-tie-low-xp', name: 'Rarity Tie Low Points', rarity: 5, - points: 10, + xp: 10, }), createUserAchievement({ id: 'locked', name: 'Locked', rarity: 1, - points: 100, + xp: 100, unlockedAt: null, }), createUserAchievement({ id: 'rare', name: 'Rare', rarity: 1, - points: 30, + xp: 30, }), createUserAchievement({ id: 'unknown-rarity', name: 'Unknown Rarity', rarity: null, - points: 100, + xp: 100, }), createUserAchievement({ - id: 'rarity-tie-high-points', + id: 'rarity-tie-high-xp', name: 'Rarity Tie High Points', rarity: 5, - points: 50, + xp: 50, }), createUserAchievement({ id: 'uncommon', name: 'Uncommon', rarity: 5, - points: 20, + xp: 20, }), ]; @@ -129,7 +129,7 @@ describe('AchievementsWidget', () => { achievements, unlockedCount: 6, totalCount: achievements.length, - totalPoints: 220, + totalXp: 220, isPending: false, isError: false, }); diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.tsx index 2e246ff8c46..6ce40dd0a3b 100644 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.tsx +++ b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.tsx @@ -55,9 +55,9 @@ function RecentAchievements({ return rarityA - rarityB; } - const pointsDelta = b.achievement.points - a.achievement.points; - if (pointsDelta !== 0) { - return pointsDelta; + const xpDelta = b.achievement.xp - a.achievement.xp; + if (xpDelta !== 0) { + return xpDelta; } const unlockedDateA = a.unlockedAt ? new Date(a.unlockedAt).getTime() : 0; diff --git a/packages/shared/src/features/profile/components/achievements/AchievementCard.spec.tsx b/packages/shared/src/features/profile/components/achievements/AchievementCard.spec.tsx index f135a067809..63c8fd500d5 100644 --- a/packages/shared/src/features/profile/components/achievements/AchievementCard.spec.tsx +++ b/packages/shared/src/features/profile/components/achievements/AchievementCard.spec.tsx @@ -15,7 +15,7 @@ const createLockedAchievement = ( image: 'https://daily.dev/achievement.png', type: AchievementType.Milestone, criteria: { targetCount: 1 }, - points: 10, + xp: 10, rarity: null, unit: null, }, diff --git a/packages/shared/src/features/profile/components/achievements/AchievementCard.tsx b/packages/shared/src/features/profile/components/achievements/AchievementCard.tsx index e04f8eaedee..9d4b5fe6c1b 100644 --- a/packages/shared/src/features/profile/components/achievements/AchievementCard.tsx +++ b/packages/shared/src/features/profile/components/achievements/AchievementCard.tsx @@ -129,7 +129,7 @@ export function AchievementCard({ } bold > - {achievement.points} + {achievement.xp} diff --git a/packages/shared/src/features/profile/components/achievements/AchievementsList.tsx b/packages/shared/src/features/profile/components/achievements/AchievementsList.tsx index 8d71b630627..125eb431512 100644 --- a/packages/shared/src/features/profile/components/achievements/AchievementsList.tsx +++ b/packages/shared/src/features/profile/components/achievements/AchievementsList.tsx @@ -180,14 +180,14 @@ export function AchievementsList({ if (!a.unlockedAt && b.unlockedAt) { return 1; } - // Among unlocked, sort by rarity (rarest first), then points (highest first) + // Among unlocked, sort by rarity (rarest first), then xp (highest first) if (a.unlockedAt && b.unlockedAt) { const rarityA = a.achievement.rarity ?? Infinity; const rarityB = b.achievement.rarity ?? Infinity; if (rarityA !== rarityB) { return rarityA - rarityB; } - return b.achievement.points - a.achievement.points; + return b.achievement.xp - a.achievement.xp; } // Among locked, sort by progress percentage (highest first) const targetA = getTargetCount(a.achievement); diff --git a/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx b/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx index 0ef65282df8..86aa2d0338a 100644 --- a/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx +++ b/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx @@ -74,7 +74,7 @@ export function ProfileAchievements({ achievements, unlockedCount, totalCount, - totalPoints, + totalXp, isPending, isError, } = useProfileAchievements(user); @@ -154,7 +154,7 @@ export function ProfileAchievements({ color={TypographyColor.Primary} bold > - {totalPoints.toLocaleString()} + {totalXp.toLocaleString()} XP diff --git a/packages/shared/src/graphql/leaderboard.ts b/packages/shared/src/graphql/leaderboard.ts index 289521a7ae2..50980e2ecb3 100644 --- a/packages/shared/src/graphql/leaderboard.ts +++ b/packages/shared/src/graphql/leaderboard.ts @@ -21,9 +21,6 @@ export const LEADERBOARD_QUERY = gql` mostReadingDays(limit: $limit) { ...LeaderboardFragment } - mostAchievementPoints(limit: $limit) { - ...LeaderboardFragment - } mostVerifiedUsers(limit: $limit) { score company { @@ -60,7 +57,6 @@ export enum LeaderboardType { MostReferrals = 'mostReferrals', MostReadingDays = 'mostReadingDays', MostVerifiedUsers = 'mostVerifiedUsers', - MostAchievementPoints = 'mostAchievementPoints', MostQuestsCompleted = 'mostQuestsCompleted', HighestLevel = 'highestLevel', } @@ -88,7 +84,6 @@ export const leaderboardTypeToTitle: Record = { [LeaderboardType.MostReferrals]: 'Most referrals', [LeaderboardType.MostReadingDays]: 'Most reading days', [LeaderboardType.MostVerifiedUsers]: 'Most verified employees', - [LeaderboardType.MostAchievementPoints]: 'Most achievement points', [LeaderboardType.MostQuestsCompleted]: 'Most quests completed', [LeaderboardType.HighestLevel]: 'Highest level', }; @@ -176,15 +171,6 @@ export const MOST_READING_DAYS_QUERY = gql` ${LEADERBOARD_FRAGMENT} `; -export const MOST_ACHIEVEMENT_POINTS_QUERY = gql` - query MostAchievementPoints($limit: Int = 100) { - mostAchievementPoints(limit: $limit) { - ...LeaderboardFragment - } - } - ${LEADERBOARD_FRAGMENT} -`; - export const MOST_QUESTS_COMPLETED_QUERY = gql` query MostQuestsCompleted($limit: Int = ${MOST_QUESTS_COMPLETED_LIMIT}) { mostQuestsCompleted(limit: $limit) { @@ -248,7 +234,6 @@ export const leaderboardQueries: Record = { [LeaderboardType.MostUpvoted]: MOST_UPVOTED_QUERY, [LeaderboardType.MostReferrals]: MOST_REFERRALS_QUERY, [LeaderboardType.MostReadingDays]: MOST_READING_DAYS_QUERY, - [LeaderboardType.MostAchievementPoints]: MOST_ACHIEVEMENT_POINTS_QUERY, [LeaderboardType.MostQuestsCompleted]: MOST_QUESTS_COMPLETED_QUERY, [LeaderboardType.HighestLevel]: HIGHEST_LEVEL_QUERY, [LeaderboardType.MostVerifiedUsers]: MOST_VERIFIED_USERS_QUERY, diff --git a/packages/shared/src/graphql/user/achievements.ts b/packages/shared/src/graphql/user/achievements.ts index 2c3ef321906..5f40acb2680 100644 --- a/packages/shared/src/graphql/user/achievements.ts +++ b/packages/shared/src/graphql/user/achievements.ts @@ -20,7 +20,7 @@ export interface Achievement { image: string; type: AchievementType; criteria?: AchievementCriteria; - points: number; + xp: number; rarity: number | null; unit: string | null; } @@ -53,8 +53,8 @@ export interface AchievementSyncStatus { } export interface AchievementSyncResult extends AchievementSyncStatus { - pointsGained: number; - totalPoints: number; + xpGained: number; + totalXp: number; newlyUnlockedAchievements: UserAchievement[]; closeAchievements: UserAchievement[]; } @@ -95,7 +95,7 @@ const ACHIEVEMENT_FRAGMENT = gql` criteria { targetCount } - points + xp rarity unit } @@ -194,8 +194,8 @@ export const SYNC_ACHIEVEMENTS_MUTATION = gql` remainingSyncs canSync syncedAchievements - pointsGained - totalPoints + xpGained + totalXp newlyUnlockedAchievements { achievement { ...AchievementFragment diff --git a/packages/shared/src/hooks/profile/useProfileAchievements.ts b/packages/shared/src/hooks/profile/useProfileAchievements.ts index 6f13871f177..5dba4fe1596 100644 --- a/packages/shared/src/hooks/profile/useProfileAchievements.ts +++ b/packages/shared/src/hooks/profile/useProfileAchievements.ts @@ -7,7 +7,7 @@ interface UseProfileAchievementsResult { achievements: UserAchievement[] | undefined; unlockedCount: number; totalCount: number; - totalPoints: number; + totalXp: number; isPending: boolean; isError: boolean; } @@ -38,16 +38,13 @@ export function useProfileAchievements( const unlocked = data?.filter((a) => a.unlockedAt !== null) ?? []; const unlockedCount = unlocked.length; const totalCount = data?.length ?? 0; - const totalPoints = unlocked.reduce( - (sum, a) => sum + (a.achievement.points ?? 0), - 0, - ); + const totalXp = unlocked.reduce((sum, a) => sum + (a.achievement.xp ?? 0), 0); return { achievements: data, unlockedCount, totalCount, - totalPoints, + totalXp, isPending, isError, }; diff --git a/packages/storybook/stories/features/snapshot/ShareImages.stories.tsx b/packages/storybook/stories/features/snapshot/ShareImages.stories.tsx index f487e95cec7..dd2a1288e93 100644 --- a/packages/storybook/stories/features/snapshot/ShareImages.stories.tsx +++ b/packages/storybook/stories/features/snapshot/ShareImages.stories.tsx @@ -172,7 +172,7 @@ const PLACEMENTS: Placement[] = [ name: `achievement-${index}`, image, }))} - points={1240} + xp={1240} seed="achievements" total={60} unlocked={18} diff --git a/packages/storybook/stories/features/snapshot/SnapshotPlacements.stories.tsx b/packages/storybook/stories/features/snapshot/SnapshotPlacements.stories.tsx index 2144687b361..d9fbf829918 100644 --- a/packages/storybook/stories/features/snapshot/SnapshotPlacements.stories.tsx +++ b/packages/storybook/stories/features/snapshot/SnapshotPlacements.stories.tsx @@ -693,7 +693,7 @@ const AchievementBox = ({ entry }: { entry: UserAchievement }) => { isUnlocked ? 'text-text-primary' : 'text-text-tertiary', )} > - {entry.achievement.points} + {entry.achievement.xp} @@ -1007,7 +1007,7 @@ const Placements = () => { step="Placement 7" leads="Snapshot" title="Achievements page — per achievement box" - note="Snapshot leads: an unlocked achievement is status with no shareable URL. Icon-only on hover, beside the points value." + note="Snapshot leads: an unlocked achievement is status with no shareable URL. Icon-only on hover, beside the xp value." >
{[ACHIEVEMENT, LOCKED_ACHIEVEMENT].map((entry) => ( diff --git a/packages/webapp/__tests__/UsersLeaderboardStaticProps.spec.ts b/packages/webapp/__tests__/UsersLeaderboardStaticProps.spec.ts index 8658fb2207c..203300764d8 100644 --- a/packages/webapp/__tests__/UsersLeaderboardStaticProps.spec.ts +++ b/packages/webapp/__tests__/UsersLeaderboardStaticProps.spec.ts @@ -31,7 +31,6 @@ const baseLeaderboardResponse = { mostUpvoted: [], mostReferrals: [], mostReadingDays: [], - mostAchievementPoints: [], mostVerifiedUsers: [], }; diff --git a/packages/webapp/lib/gameCenter.spec.ts b/packages/webapp/lib/gameCenter.spec.ts index 46a2cdfa432..090b762485a 100644 --- a/packages/webapp/lib/gameCenter.spec.ts +++ b/packages/webapp/lib/gameCenter.spec.ts @@ -47,7 +47,7 @@ const createAchievement = ( overrides: Partial & { id: string; name: string; - points?: number; + xp?: number; }, ): UserAchievement => ({ achievement: { @@ -57,7 +57,7 @@ const createAchievement = ( image: 'https://daily.dev/achievement.png', type: AchievementType.Milestone, criteria: { targetCount: 10 }, - points: overrides.points ?? 100, + xp: overrides.xp ?? 100, rarity: 10, unit: 'posts', }, @@ -214,13 +214,13 @@ describe('game center helpers', () => { id: 'tracked', name: 'Tracked', progress: 9, - points: 50, + xp: 50, }); const rareUnlocked = createAchievement({ id: 'rare', name: 'Rare unlocked', unlockedAt: '2025-03-01T00:00:00.000Z', - points: 200, + xp: 200, achievement: { id: 'rare', name: 'Rare unlocked', @@ -228,7 +228,7 @@ describe('game center helpers', () => { image: 'https://daily.dev/rare.png', type: AchievementType.Milestone, criteria: { targetCount: 10 }, - points: 200, + xp: 200, rarity: 1, unit: 'posts', }, @@ -237,7 +237,7 @@ describe('game center helpers', () => { id: 'latest', name: 'Latest unlocked', unlockedAt: '2025-03-10T00:00:00.000Z', - points: 120, + xp: 120, }); const summary = getAchievementSummary( @@ -247,7 +247,7 @@ describe('game center helpers', () => { expect(summary.unlockedCount).toBe(2); expect(summary.totalCount).toBe(3); - expect(summary.totalPoints).toBe(320); + expect(summary.totalXp).toBe(320); expect(summary.nextToUnlock?.achievement.id).toBe('tracked'); expect(summary.latestUnlocked?.achievement.id).toBe('latest'); expect(summary.rarestUnlocked?.achievement.id).toBe('rare'); diff --git a/packages/webapp/lib/gameCenter.ts b/packages/webapp/lib/gameCenter.ts index 1ab04e5eab8..c6c61045adc 100644 --- a/packages/webapp/lib/gameCenter.ts +++ b/packages/webapp/lib/gameCenter.ts @@ -185,7 +185,7 @@ const dedupeAchievements = ( export type GameCenterAchievementSummary = { unlockedCount: number; totalCount: number; - totalPoints: number; + totalXp: number; latestUnlocked: UserAchievement | null; rarestUnlocked: UserAchievement | null; nextToUnlock: UserAchievement | null; @@ -235,7 +235,7 @@ export const getAchievementSummary = ( return right.progress - left.progress; } - return right.achievement.points - left.achievement.points; + return right.achievement.xp - left.achievement.xp; })[0] ?? null; const featuredAchievements = dedupeAchievements([ @@ -248,8 +248,8 @@ export const getAchievementSummary = ( return { unlockedCount: unlocked.length, totalCount: allAchievements.length, - totalPoints: unlocked.reduce( - (total, achievement) => total + (achievement.achievement.points ?? 0), + totalXp: unlocked.reduce( + (total, achievement) => total + (achievement.achievement.xp ?? 0), 0, ), latestUnlocked, diff --git a/packages/webapp/pages/users.tsx b/packages/webapp/pages/users.tsx index 13d02f08183..1a46ccbbdf8 100644 --- a/packages/webapp/pages/users.tsx +++ b/packages/webapp/pages/users.tsx @@ -45,7 +45,6 @@ interface PageProps { mostUpvoted: UserLeaderboard[]; mostReferrals: UserLeaderboard[]; mostReadingDays: UserLeaderboard[]; - mostAchievementPoints: UserLeaderboard[]; highestLevel: UserLeaderboard[]; isHighestLevelSupported: boolean; mostVerifiedUsers: CompanyLeaderboard[]; @@ -77,7 +76,6 @@ const LeaderboardPage = ({ mostUpvoted, mostReferrals, mostReadingDays, - mostAchievementPoints, highestLevel, isHighestLevelSupported, mostVerifiedUsers, @@ -167,14 +165,6 @@ const LeaderboardPage = ({ isLoading={isLoading} leaderboardType={LeaderboardType.MostReadingDays} /> - Date: Fri, 11 Sep 2026 15:18:08 +0200 Subject: [PATCH 2/3] fix(achievements): disambiguate achievement XP and redirect the retired board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on the API rename: - AchievementSyncResult.totalXp is now totalAchievementXp, matching daily-api. The same collision existed client side, so useProfileAchievements and the Game Center summary rename too — all three count achievement XP only, while questDashboard.level.totalXp is the whole balance. - /users/mostAchievementPoints permanently redirects to /users/highestLevel instead of 404ing, since that board is the superset this PR retires it for. Co-Authored-By: Claude Opus 5 (1M context) --- .../filters/AchievementTrackerButton.spec.tsx | 2 +- .../ProfileWidgets/AchievementSyncModal.tsx | 2 +- .../ProfileWidgets/AchievementsWidget.spec.tsx | 2 +- .../components/achievements/ProfileAchievements.tsx | 4 ++-- packages/shared/src/graphql/user/achievements.ts | 5 +++-- .../src/hooks/profile/useProfileAchievements.ts | 10 +++++++--- packages/webapp/lib/gameCenter.spec.ts | 2 +- packages/webapp/lib/gameCenter.ts | 5 +++-- packages/webapp/pages/users/[id].tsx | 13 +++++++++++++ 9 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx b/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx index 603b84ddfb7..a6637c752b3 100644 --- a/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx +++ b/packages/shared/src/components/filters/AchievementTrackerButton.spec.tsx @@ -123,7 +123,7 @@ const defaultProfileAchievementsHook = { achievements: [mockTrackedAchievement], unlockedCount: 0, totalCount: 1, - totalXp: 0, + totalAchievementXp: 0, isPending: false, isError: false, }; diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx index e257ec6398f..283b5a8688f 100644 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx +++ b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementSyncModal.tsx @@ -152,7 +152,7 @@ export const AchievementSyncModal = ({ return; } - const baseScore = result.totalXp - result.xpGained; + const baseScore = result.totalAchievementXp - result.xpGained; setScore(baseScore); setCurrentIndex(0); diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx index ac3dc2f6c04..831fd9e867d 100644 --- a/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx +++ b/packages/shared/src/features/profile/components/ProfileWidgets/AchievementsWidget.spec.tsx @@ -129,7 +129,7 @@ describe('AchievementsWidget', () => { achievements, unlockedCount: 6, totalCount: achievements.length, - totalXp: 220, + totalAchievementXp: 220, isPending: false, isError: false, }); diff --git a/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx b/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx index 86aa2d0338a..568ab497bb0 100644 --- a/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx +++ b/packages/shared/src/features/profile/components/achievements/ProfileAchievements.tsx @@ -74,7 +74,7 @@ export function ProfileAchievements({ achievements, unlockedCount, totalCount, - totalXp, + totalAchievementXp, isPending, isError, } = useProfileAchievements(user); @@ -154,7 +154,7 @@ export function ProfileAchievements({ color={TypographyColor.Primary} bold > - {totalXp.toLocaleString()} XP + {totalAchievementXp.toLocaleString()} XP
a.unlockedAt !== null) ?? []; const unlockedCount = unlocked.length; const totalCount = data?.length ?? 0; - const totalXp = unlocked.reduce((sum, a) => sum + (a.achievement.xp ?? 0), 0); + const totalAchievementXp = unlocked.reduce( + (sum, a) => sum + (a.achievement.xp ?? 0), + 0, + ); return { achievements: data, unlockedCount, totalCount, - totalXp, + totalAchievementXp, isPending, isError, }; diff --git a/packages/webapp/lib/gameCenter.spec.ts b/packages/webapp/lib/gameCenter.spec.ts index 090b762485a..89c562040dd 100644 --- a/packages/webapp/lib/gameCenter.spec.ts +++ b/packages/webapp/lib/gameCenter.spec.ts @@ -247,7 +247,7 @@ describe('game center helpers', () => { expect(summary.unlockedCount).toBe(2); expect(summary.totalCount).toBe(3); - expect(summary.totalXp).toBe(320); + expect(summary.totalAchievementXp).toBe(320); expect(summary.nextToUnlock?.achievement.id).toBe('tracked'); expect(summary.latestUnlocked?.achievement.id).toBe('latest'); expect(summary.rarestUnlocked?.achievement.id).toBe('rare'); diff --git a/packages/webapp/lib/gameCenter.ts b/packages/webapp/lib/gameCenter.ts index c6c61045adc..0f48aa88ce5 100644 --- a/packages/webapp/lib/gameCenter.ts +++ b/packages/webapp/lib/gameCenter.ts @@ -185,7 +185,8 @@ const dedupeAchievements = ( export type GameCenterAchievementSummary = { unlockedCount: number; totalCount: number; - totalXp: number; + /** XP from unlocked achievements only, not the user's whole balance. */ + totalAchievementXp: number; latestUnlocked: UserAchievement | null; rarestUnlocked: UserAchievement | null; nextToUnlock: UserAchievement | null; @@ -248,7 +249,7 @@ export const getAchievementSummary = ( return { unlockedCount: unlocked.length, totalCount: allAchievements.length, - totalXp: unlocked.reduce( + totalAchievementXp: unlocked.reduce( (total, achievement) => total + (achievement.achievement.xp ?? 0), 0, ), diff --git a/packages/webapp/pages/users/[id].tsx b/packages/webapp/pages/users/[id].tsx index bb091528c5a..a812003bf30 100644 --- a/packages/webapp/pages/users/[id].tsx +++ b/packages/webapp/pages/users/[id].tsx @@ -44,6 +44,8 @@ interface PageProps extends DynamicSeoProps { companyItems?: CompanyLeaderboard[]; } +const RETIRED_ACHIEVEMENT_POINTS_SLUG = 'mostAchievementPoints'; + const getLeaderboardLimit = (leaderboardType: LeaderboardType): number => leaderboardType === LeaderboardType.MostQuestsCompleted ? MOST_QUESTS_COMPLETED_LIMIT @@ -158,6 +160,17 @@ export async function getStaticProps({ > { const { id } = params || {}; + // Achievement points were folded into XP, so highest level is now the + // superset of this board. Bookmarks and shared links land here. + if (id === RETIRED_ACHIEVEMENT_POINTS_SLUG) { + return { + redirect: { + destination: `/users/${LeaderboardType.HighestLevel}`, + permanent: true, + }, + }; + } + if (!id || !Object.values(LeaderboardType).includes(id as LeaderboardType)) { return { notFound: true, From 696c270d0db3a58034eb44327793e6e448807c5f Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 11 Sep 2026 15:39:47 +0200 Subject: [PATCH 3/3] fix(achievements): satisfy the strict typecheck in CompareAchievementsModal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typecheck_strict_changed failed on CI: touching this file for the XP copy pulled it into the changed set, and the strict gate then surfaced two violations that predate this PR. - handleClose took an optional event but onRequestClose requires one; both call sites always pass an event, so drop the optional marker rather than widen the contract - ProfilePicture requires a user and useAuthContext().user is optional, so only render the avatar when there is one — the name beside it already falls back to "You" Co-Authored-By: Claude Opus 5 (1M context) --- .../modals/achievement/CompareAchievementsModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/modals/achievement/CompareAchievementsModal.tsx b/packages/shared/src/components/modals/achievement/CompareAchievementsModal.tsx index eefd17ddc0f..bc901409f91 100644 --- a/packages/shared/src/components/modals/achievement/CompareAchievementsModal.tsx +++ b/packages/shared/src/components/modals/achievement/CompareAchievementsModal.tsx @@ -112,7 +112,7 @@ export const CompareAchievementsModal = ({ }; }, [myAchievements, profileAchievements]); - const handleClose = (event?: React.MouseEvent | React.KeyboardEvent): void => + const handleClose = (event: React.MouseEvent | React.KeyboardEvent): void => onRequestClose?.(event); return ( @@ -127,7 +127,9 @@ export const CompareAchievementsModal = ({
- + {loggedUser && ( + + )}