Feature/achievement handling (#2)

* Add NewAchievement type to ScoreBreakdown

* Show toast on achievement unlock after game completion

* Show toast on achievement unlock after versus win
This commit is contained in:
2026-03-15 17:04:48 -07:00
committed by GitHub
parent c80fe41bc9
commit f972de718c
3 changed files with 41 additions and 2 deletions
+11
View File
@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { toast } from 'sonner';
import { getMovieDetails } from '@/api/movies';
import type {
ChainLink,
@@ -210,6 +211,16 @@ export const useGameStore = create<GameState>((set, get) => ({
completeGameSession(sessionId, newChain, hintsUsed)
.then((backendScore) => {
set({ score: backendScore });
// Show toast for each newly earned achievement
if (backendScore.newAchievements?.length) {
for (const achievement of backendScore.newAchievements) {
toast.success(`Achievement Unlocked: ${achievement.name}`, {
description: achievement.description,
duration: 5000,
});
}
}
})
.catch(() => {
set({ validationError: 'Failed to calculate score.' });
+22 -2
View File
@@ -1,7 +1,9 @@
import { create } from 'zustand';
import { toast } from 'sonner';
import type { Socket } from 'socket.io-client';
import { createSocket } from '@/lib/create-socket';
import type { ScoreBreakdown } from '@/types';
import { useAuthStore } from '@/stores/auth-store';
import type { ScoreBreakdown, NewAchievement } from '@/types';
export type LobbyState =
| 'idle'
@@ -117,7 +119,14 @@ export const useVersusStore = create<VersusState>((set, get) => ({
set({ opponentFinished: true });
});
socket.on('match-finished', (data) => {
socket.on('match-finished', (data: {
winnerId: string | null;
player1: { id: string; username: string };
player2: { id: string; username: string };
player1Score: ScoreBreakdown | null;
player2Score: ScoreBreakdown | null;
newAchievements?: NewAchievement[];
}) => {
set({
lobbyState: 'finished',
matchResult: {
@@ -128,6 +137,17 @@ export const useVersusStore = create<VersusState>((set, get) => ({
player2Score: data.player2Score,
},
});
// Show achievement toasts if the current user is the winner
const currentUserId = useAuthStore.getState().user?.id;
if (data.newAchievements?.length && currentUserId === data.winnerId) {
for (const achievement of data.newAchievements) {
toast.success(`Achievement Unlocked: ${achievement.name}`, {
description: achievement.description,
duration: 5000,
});
}
}
});
socket.on('match-cancelled', () => {
+8
View File
@@ -29,6 +29,13 @@ export interface PresetMoviePair extends MoviePair {
description?: string;
}
export interface NewAchievement {
key: string;
name: string;
description: string;
icon: string;
}
export interface ScoreBreakdown {
baseScore: number;
chainLength: number;
@@ -41,6 +48,7 @@ export interface ScoreBreakdown {
elapsedSeconds: number;
totalScore: number;
par?: number;
newAchievements?: NewAchievement[];
}
/** Number of actor-movie pair "links" in a chain (excludes starting movie) */