Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion redux/selectors.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InteractionType } from '../src/components/Interactions/InteractionType';

import { selectInteractionType, selectCurrentGame, selectLastStoreReviewPrompt } from './selectors';
import { selectCurrentGame, selectGamePlayersByScore, selectInteractionType, selectLastStoreReviewPrompt } from './selectors';
import { RootState } from './store';

// Mock data for testing
Expand Down Expand Up @@ -195,4 +195,43 @@ describe('Redux selectors', () => {
expect(result).toBe(9876543210);
});
});

describe('selectGamePlayersByScore', () => {
it('should return game players sorted by total score descending', () => {
const state = {
...mockState,
games: {
entities: {
'game-1': {
...mockState.games!.entities['game-1'],
playerIds: ['player-1', 'player-2', 'player-3'],
},
},
ids: ['game-1'],
},
players: {
entities: {
'player-1': { id: 'player-1', playerName: 'Alex', scores: [2, 3] },
'player-2': { id: 'player-2', playerName: 'Blair', scores: [7, 1] },
'player-3': { id: 'player-3', playerName: 'Casey', scores: [4] },
},
ids: ['player-1', 'player-2', 'player-3'],
},
} as RootState;

expect(selectGamePlayersByScore(state, 'game-1')).toEqual([
{ id: 'player-2', name: 'Blair', totalScore: 8 },
{ id: 'player-1', name: 'Alex', totalScore: 5 },
{ id: 'player-3', name: 'Casey', totalScore: 4 },
]);
});

it('should preserve missing player ids with empty summary values', () => {
const state = mockState as RootState;

expect(selectGamePlayersByScore(state, 'game-1')).toEqual([
{ id: 'player-1', name: '', totalScore: 0 },
]);
});
});
});
28 changes: 28 additions & 0 deletions redux/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { createSelector } from '@reduxjs/toolkit';

import { InteractionType } from '../src/components/Interactions/InteractionType';

import { selectGameById } from './GamesSlice';
import { RootState } from './store';

export interface PlayerScoreSummary {
id: string;
name: string;
totalScore: number;
}

const EMPTY_PLAYER_IDS: string[] = [];

export const selectInteractionType = (state: RootState, gameId?: string): InteractionType => {
// Prefer the game-level gesture; fall back to the global default for new games.
const gameLevel = gameId ? selectGameById(state, gameId)?.interactionType : undefined;
Expand All @@ -23,4 +33,22 @@ export const selectCurrentGame = (state: RootState) => {

return selectGameById(state, currentGameId);
};

export const selectGamePlayersByScore = createSelector(
[
(state: RootState, gameId: string | undefined) => gameId ? state.games.entities[gameId]?.playerIds ?? EMPTY_PLAYER_IDS : EMPTY_PLAYER_IDS,
(state: RootState) => state.players.entities,
],
(playerIds, players): PlayerScoreSummary[] => playerIds
.map((id) => {
const player = players[id];
return {
id,
name: player?.playerName || '',
totalScore: (player?.scores || []).reduce((total, score) => total + (score || 0), 0),
};
})
.sort((a, b) => b.totalScore - a.totalScore)
);

export const selectLastStoreReviewPrompt = (state: RootState) => state.settings.lastStoreReviewPrompt;
51 changes: 10 additions & 41 deletions src/components/Sheets/ChooseWinnersSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Icon } from 'react-native-elements';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { shallowEqual } from 'react-redux';

import { selectGameById, updateGame } from '../../../redux/GamesSlice';
import { updateGame } from '../../../redux/GamesSlice';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { selectGamePlayersByScore } from '../../../redux/selectors';
import { logEvent } from '../../Analytics';
import { useTheme } from '../../theme';

Expand All @@ -26,37 +26,7 @@ const ChooseWinnersSheet: React.FunctionComponent = () => {
const topInset = insets.top + 50;

const currentGameId = useAppSelector(state => state.settings.currentGameId);
const playerIds = useAppSelector(state => selectGameById(state, currentGameId || '')?.playerIds);
const allPlayers = useAppSelector((state) =>
(playerIds || []).map((id) => state.players.entities[id]),
shallowEqual
);
const sortedPlayerIds = useMemo(() => {
const withScores = (playerIds || []).map((id) => {
const p = allPlayers.find((ap) => ap?.id === id);

return {
id,
totalScore: (p?.scores || []).reduce((a, b) => a + b, 0),
};
});

withScores.sort((a, b) => b.totalScore - a.totalScore);

return withScores.map((p) => p.id);
}, [allPlayers, playerIds]);
const playerInfo = useMemo(
() =>
Object.fromEntries(
allPlayers.map((p) => [
p?.id,
{
name: p?.playerName || '',
totalScore: (p?.scores || []).reduce((a, b) => a + b, 0),
},
])),
[allPlayers]
);
const playersByScore = useAppSelector(state => selectGamePlayersByScore(state, currentGameId));

const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

Expand Down Expand Up @@ -162,16 +132,15 @@ const ChooseWinnersSheet: React.FunctionComponent = () => {
</Text>

<View style={[styles.playerList, { backgroundColor: theme.backgroundSecondary }]}>
{sortedPlayerIds.map((playerId, index) => {
const info = playerInfo[playerId];
const isSelected = selectedIds.has(playerId);
const isLast = index === sortedPlayerIds.length - 1;
{playersByScore.map((player, index) => {
const isSelected = selectedIds.has(player.id);
const isLast = index === playersByScore.length - 1;

return (
<React.Fragment key={playerId}>
<React.Fragment key={player.id}>
<TouchableOpacity
style={styles.playerRow}
onPress={() => togglePlayer(playerId)}
onPress={() => togglePlayer(player.id)}
activeOpacity={0.6}
testID={`winner-player-row-${index}`}
>
Expand All @@ -182,10 +151,10 @@ const ChooseWinnersSheet: React.FunctionComponent = () => {
size={22}
/>
<Text style={[styles.playerName, { color: theme.text }]} numberOfLines={1}>
{info?.name}
{player.name}
</Text>
<Text style={[styles.playerScore, { color: theme.textSecondary }]}>
{info?.totalScore}
{player.totalScore}
</Text>
</TouchableOpacity>
{!isLast && (
Expand Down