diff --git a/db-example.json b/db-example.json index 5ea5dd6..20fef42 100644 --- a/db-example.json +++ b/db-example.json @@ -52,5 +52,6 @@ "text": "Win a game without losing a single point", "title": "FATALITY!" } - } + }, + "doubles_pair": "" } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index b9e33a3..01fcbe9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,8 @@ function App(): JSX.Element {
- + + diff --git a/src/api/ApiTypes.ts b/src/api/ApiTypes.ts index d2d040d..6fb849f 100644 --- a/src/api/ApiTypes.ts +++ b/src/api/ApiTypes.ts @@ -10,6 +10,7 @@ export enum HttpMethod { export enum ApiActions { INSTANCES = "instances.json", PLAYERS = "player.json", + DOUBLES_PAIRS = "doubles_pair.json", MATCHES = "match.json", HAPPY_HOUR = "happyhour.json", BADGE = "badge.json", diff --git a/src/api/QuickHitAPI.ts b/src/api/QuickHitAPI.ts index 32a9d46..483b61a 100644 --- a/src/api/QuickHitAPI.ts +++ b/src/api/QuickHitAPI.ts @@ -1,6 +1,7 @@ import { DbBadge, DbChatRoom, + DbDoublesPair, DbHappyHour, DbInstance, DbMatch, @@ -46,6 +47,19 @@ export class QuickHitAPI { }); } + public static getDoublesPairs( + onSuccess: (doublesPairs: DbDoublesPair[]) => void, + onFailure: (errorString: string) => void + ): void { + QuickHitAPI.makeAxiosRequest(ApiActions.DOUBLES_PAIRS, HttpMethod.GET) + .then((response: AxiosResponse) => { + onSuccess(Object.values(response.data)); + }) + .catch((error: AxiosError) => { + onFailure(error.message); + }); + } + public static getMatches(onSuccess: (matches: DbMatch[]) => void, onFailure: (errorString: string) => void): void { QuickHitAPI.makeAxiosRequest(ApiActions.MATCHES, HttpMethod.GET) .then((response: AxiosResponse) => { @@ -111,6 +125,24 @@ export class QuickHitAPI { }); } + public static addOrUpdateDoublesPair( + pairToAdd: DbDoublesPair, + onSuccess: () => void, + onFailure: (errorString: string) => void + ): void { + QuickHitAPI.makeAxiosRequest( + ApiActions.DOUBLES_PAIRS, + HttpMethod.PATCH, + `{"${pairToAdd.id}" : ${JSON.stringify(pairToAdd)}}` + ) + .then(() => { + onSuccess(); + }) + .catch((error: AxiosError) => { + onFailure(error.message); + }); + } + public static getTodaysHappyHour( onSuccess: (happyHour?: DbHappyHour) => void, onFailure: (errorString: string) => void @@ -178,8 +210,9 @@ export class QuickHitAPI { public static addNewMatch( matchToAdd: DbMatch, - winningPlayer: DbPlayer, - losingPlayer: DbPlayer, + winningPlayer: DbPlayer | DbDoublesPair, + losingPlayer: DbPlayer | DbDoublesPair, + doubles: boolean, onSuccess: () => void, onFailure: (errorString: string) => void ): void { @@ -189,20 +222,37 @@ export class QuickHitAPI { `{"${matchToAdd.id}" : ${JSON.stringify(matchToAdd)}}` ) .then(() => { - this.addOrUpdatePlayer( - winningPlayer, - () => { - return; - }, - onFailure - ); - this.addOrUpdatePlayer( - losingPlayer, - () => { - return; - }, - onFailure - ); + if (doubles) { + this.addOrUpdateDoublesPair( + winningPlayer as DbDoublesPair, + () => { + return; + }, + onFailure + ); + this.addOrUpdateDoublesPair( + losingPlayer as DbDoublesPair, + () => { + return; + }, + onFailure + ); + } else { + this.addOrUpdatePlayer( + winningPlayer, + () => { + return; + }, + onFailure + ); + this.addOrUpdatePlayer( + losingPlayer, + () => { + return; + }, + onFailure + ); + } onSuccess(); }) .catch((error: AxiosError) => { diff --git a/src/components/Comparator/Comparator.tsx b/src/components/Comparator/Comparator.tsx index c2805f1..39bb3e8 100644 --- a/src/components/Comparator/Comparator.tsx +++ b/src/components/Comparator/Comparator.tsx @@ -4,7 +4,7 @@ import "./Comparator.css"; import { DbPlayer, getELOString } from "../../types/database/models"; import { ComparatorStoreProps } from "../../containers/Comparator/Comparator"; import { WinLoss } from "../../types/types"; -import { getRecordAgainstPlayer, getWinLossForPlayer } from "../QHDataLoader/QHDataLoader"; +import { getRecordAgainstPlayer, getWinLossForPlayerOrPair } from "../QHDataLoader/QHDataLoader"; import { getChanceOfVictory } from "../../util/Predictor"; /** @@ -108,12 +108,18 @@ function Comparator(props: ComparatorStoreProps): JSX.Element { {playerOne && - getELOString(getWinLossForPlayer(playerOne.id, props.matches).matches, playerOne.elo)} + getELOString( + getWinLossForPlayerOrPair(playerOne.id, props.matches).matches, + playerOne.elo + )} ELO {playerTwo && - getELOString(getWinLossForPlayer(playerTwo.id, props.matches).matches, playerTwo.elo)} + getELOString( + getWinLossForPlayerOrPair(playerTwo.id, props.matches).matches, + playerTwo.elo + )} diff --git a/src/components/HallOfFallen/HallOfFallen.test.tsx b/src/components/HallOfFallen/HallOfFallen.test.tsx index 62abd76..57f75f6 100644 --- a/src/components/HallOfFallen/HallOfFallen.test.tsx +++ b/src/components/HallOfFallen/HallOfFallen.test.tsx @@ -2,5 +2,5 @@ import { render } from "@testing-library/react"; import HallOfFallen from "./HallOfFallen"; it("renders without crashing", () => { - render(); + render(); }); diff --git a/src/components/HallOfFallen/HallOfFallen.tsx b/src/components/HallOfFallen/HallOfFallen.tsx index 46b1925..d0cd1a2 100644 --- a/src/components/HallOfFallen/HallOfFallen.tsx +++ b/src/components/HallOfFallen/HallOfFallen.tsx @@ -14,7 +14,11 @@ import "./HallOfFallen.css"; function HallOfFallen(props: HallOfFallenReduxProps & TTRefreshDispatchType): JSX.Element { const [retirees, setRetirees] = useState([]); - useEffect(() => setRetirees(props.players.filter((player) => player.retired === true)), [props.players]); + useEffect(() => { + const retirees = props.players.filter((player) => player.retired === true); + retirees.push(...(props.doublesPairs.filter((doublesPair) => doublesPair.retired === true) as DbPlayer[])); + setRetirees(retirees); + }, [props.players, props.doublesPairs]); const renderItems = (): JSX.Element[] => { const retireeItems: JSX.Element[] = []; diff --git a/src/components/KeyPrompt/KeyPrompt.tsx b/src/components/KeyPrompt/KeyPrompt.tsx index 52730f8..cc47cd5 100644 --- a/src/components/KeyPrompt/KeyPrompt.tsx +++ b/src/components/KeyPrompt/KeyPrompt.tsx @@ -9,7 +9,6 @@ import * as firebaseAuth from "firebase/auth"; import { AuthUserDetail } from "../../redux/types/AuthTypes"; import ReactGA from "react-ga"; import { match } from "react-router"; -import * as H from "history"; import { BASE_PATH, QuickHitPage } from "../../util/QuickHitPage"; export interface KeyPromptMatchParams { @@ -24,8 +23,6 @@ export interface KeyPromptProps { setAuthKey: (newKey: string) => void; setChosenInstance: (newInstance: DbInstance) => void; setAuthDetail: (newAuthDetail?: AuthUserDetail) => void; - history?: H.History; - location?: H.Location; match?: match; } @@ -120,8 +117,7 @@ function KeyPrompt(props: KeyPromptProps): JSX.Element { }; }; - // Todo: use quickhitpage later - if (props.match && !(props.match.params.instance === "player")) { + if (props.match) { const instanceId = props.match.params.instance; const authKey = props.match.params.authKey; diff --git a/src/components/Ladder/Ladder.css b/src/components/Ladder/Ladder.css index 721671c..5a5e04b 100644 --- a/src/components/Ladder/Ladder.css +++ b/src/components/Ladder/Ladder.css @@ -10,6 +10,17 @@ display: block; } +.players .header .child.icon.first-child { + margin-left: -40px; + font-size: 3.6rem; +} + +.players .header .child.icon.second-child { + font-size: 3.6rem; + margin-left: 40px; + margin-top: -58px; +} + .horizontal .card { display: inline-flex; margin-left: 5px; diff --git a/src/components/Ladder/Ladder.test.tsx b/src/components/Ladder/Ladder.test.tsx index cc55008..fe5a40e 100644 --- a/src/components/Ladder/Ladder.test.tsx +++ b/src/components/Ladder/Ladder.test.tsx @@ -23,7 +23,7 @@ it("renders and runs connect without crashing", () => { render( - + ); diff --git a/src/components/Ladder/Ladder.tsx b/src/components/Ladder/Ladder.tsx index ea61db8..a5040dd 100644 --- a/src/components/Ladder/Ladder.tsx +++ b/src/components/Ladder/Ladder.tsx @@ -3,72 +3,89 @@ import { Button, Header, Icon, Table, Transition } from "semantic-ui-react"; import PlayerCard from "./PlayerCard/PlayerCard"; import NewEditPlayer from "../NewEditPlayer/NewEditPlayer"; import NewGame from "../../containers/NewGame"; -import { getWinLossForPlayer } from "../QHDataLoader/QHDataLoader"; +import { getPlayersMap, getWinLossForPlayerOrPair } from "../QHDataLoader/QHDataLoader"; import { TTDataPropsTypeCombined } from "../../containers/shared"; import { BASE_PATH, QuickHitPage } from "../../util/QuickHitPage"; import { Link } from "react-router-dom"; import { ViewDispatchType } from "../../containers/Ladder/Ladder"; import { ViewStoreState } from "../../redux/types/ViewTypes"; -import { DbPlayer, getELOString, isUnderPlacement } from "../../types/database/models"; +import { DbDoublesPair, DbPlayer, getELOString, isUnderPlacement } from "../../types/database/models"; +import * as H from "history"; + +export type LadderProps = ViewStoreState & + TTDataPropsTypeCombined & + ViewDispatchType & { + location: H.Location; + }; -export type LadderProps = ViewStoreState & TTDataPropsTypeCombined & ViewDispatchType; export const NUM_OF_FORM_GUIDE_MATCHES = 5; /** * QuickHit Ladder page. */ function Ladder(props: LadderProps): JSX.Element { + const isDoubles = props.location.pathname === `${BASE_PATH()}${QuickHitPage.DOUBLES_LADDER}`; + const renderPlayersAsCards = (): JSX.Element[] => { - const playersLadder: JSX.Element[] = []; + const ladder: JSX.Element[] = []; - props.players.forEach((player) => { - if (!player.retired) { - const winLoss = getWinLossForPlayer(player.id, props.matches); + const iterable = isDoubles ? props.doublesPairs : props.players; - const playerCard = ; + iterable.forEach((playerOrDoublesPair) => { + if (!playerOrDoublesPair.retired) { + const winLoss = getWinLossForPlayerOrPair(playerOrDoublesPair.id, props.matches); + + const playerCard = ( + + ); // If we are hiding zero game players, then only push if they have played a game if (props.hideUnplacedPlayers) { if (!isUnderPlacement(winLoss.wins + winLoss.losses)) { - playersLadder.push(playerCard); + ladder.push(playerCard); } } else { - playersLadder.push(playerCard); + ladder.push(playerCard); } } }); // Sorting the player items by elo. - playersLadder.sort((player1, player2) => { + ladder.sort((player1, player2) => { return player2.props.player.elo - player1.props.player.elo; }); - return playersLadder; + return ladder; }; const renderPlayersInTable = (): JSX.Element[] => { - const playersLadder: JSX.Element[] = []; - const playerTableRows: JSX.Element[] = []; + const ladder: JSX.Element[] = []; + const tableRows: JSX.Element[] = []; - const sortedPlayers = props.players.sort((player1, player2) => { - return player2.elo - player1.elo; + const iterable = isDoubles ? props.doublesPairs : props.players; + + const sortedIterable = iterable.sort((playerOrPair1, playerOrPair2) => { + return playerOrPair2.elo - playerOrPair1.elo; }); - const unplacedPlayerRows = []; + const unplacedPairRows = []; - for (let i = 0; i < sortedPlayers.length; i++) { - const player = sortedPlayers[i]; - const winLoss = getWinLossForPlayer(player.id, props.matches); - let addPlayer = true; + for (let i = 0; i < sortedIterable.length; i++) { + const playerOrPair = sortedIterable[i]; + const winLoss = getWinLossForPlayerOrPair(playerOrPair.id, props.matches); + let addPlayerOrPair = true; if ( (props.hideUnplacedPlayers && isUnderPlacement(winLoss.wins + winLoss.losses)) || - player.retired === true + playerOrPair.retired === true ) { - addPlayer = false; + addPlayerOrPair = false; } - if (addPlayer) { + if (addPlayerOrPair) { + const playersMap = getPlayersMap(props.players); + const doublesPair = playerOrPair as DbDoublesPair; + const formStr = winLoss && winLoss.formGuide.substr(0, NUM_OF_FORM_GUIDE_MATCHES).split("").reverse().join(""); @@ -77,16 +94,22 @@ function Ladder(props: LadderProps): JSX.Element { - {generateLadderTrendIcon(player, i, sortedPlayers)} - - {player.name} + {generateLadderTrendIcon(playerOrPair, i, sortedIterable)} + + {playerOrPair.name} - {getELOString(winLoss.wins + winLoss.losses, player.elo)} + {isDoubles && ( + + {playersMap.get(doublesPair.player1_id)?.name} &{" "} + {playersMap.get(doublesPair.player2_id)?.name} + + )} + {getELOString(winLoss.wins + winLoss.losses, playerOrPair.elo)} {winLoss.wins}-{winLoss.losses} @@ -95,36 +118,37 @@ function Ladder(props: LadderProps): JSX.Element { ); if (isUnderPlacement(winLoss.wins + winLoss.losses)) { - unplacedPlayerRows.push(row); + unplacedPairRows.push(row); } else { - playerTableRows.push(row); + tableRows.push(row); } } } - playersLadder.push( + ladder.push( - Player + {isDoubles ? "Team" : "Player"} + {isDoubles && Members} ELO W-L Form - {playerTableRows} - {unplacedPlayerRows} + {tableRows} + {unplacedPairRows}
); - return playersLadder; + return ladder; }; const generateLadderTrendIcon = ( - player: DbPlayer, + playerOrPair: DbPlayer | DbDoublesPair, positionOnLadder: number, - sortedPlayers: DbPlayer[] + sortedPlayersOrPairs: DbPlayer[] | DbDoublesPair[] ): JSX.Element => { let mostRecentMatch; let iconToReturn: JSX.Element = ; @@ -132,23 +156,23 @@ function Ladder(props: LadderProps): JSX.Element { // Find player's most recent match (assumes matches already sorted by newest) for (let i = 0; i < props.matches.length; i++) { const match = props.matches[i]; - if (match.winning_player_id === player.id || match.losing_player_id === player.id) { + if (match.winning_player_id === playerOrPair.id || match.losing_player_id === playerOrPair.id) { mostRecentMatch = match; break; } } - const winLoss = getWinLossForPlayer(player.id, props.matches); + const winLoss = getWinLossForPlayerOrPair(playerOrPair.id, props.matches); if (isUnderPlacement(winLoss.wins + winLoss.losses)) { iconToReturn = ; } else if (mostRecentMatch) { // If the most recent match was a loss - if (mostRecentMatch.losing_player_id === player.id) { + if (mostRecentMatch.losing_player_id === playerOrPair.id) { if (positionOnLadder !== 0) { let wentDown = false; // Get the player above them - const playerAboveOnLadder = sortedPlayers[positionOnLadder - 1]; + const playerAboveOnLadder = sortedPlayersOrPairs[positionOnLadder - 1]; // If the player above them was the player they versed, then use that player's original elo. if ( @@ -176,7 +200,7 @@ function Ladder(props: LadderProps): JSX.Element { // Otherwise, match was a win else { // Get the player below them - const playerBelowOnLadder = sortedPlayers[positionOnLadder + 1]; + const playerBelowOnLadder = sortedPlayersOrPairs[positionOnLadder + 1]; let wentUp = false; // If the player below them was the player they versed, then use that player's original elo. @@ -220,8 +244,25 @@ function Ladder(props: LadderProps): JSX.Element { return (
- - Ladder + {isDoubles ? ( + + + + + + } + circular + /> + Doubles ladder + + ) : ( + + + Singles ladder + + )}
) } @@ -135,21 +170,85 @@ function NewEditPlayer(props: NewEditPlayerProps): JSX.Element { {props.editingPlayer ? ( - Edit Player + {props.doublesOnly ? "Edit Doubles Team" : "Edit Player"} ) : ( - New Player + {props.doublesOnly ? "New Doubles Team" : "New Player"} )}
+ {props.doublesOnly && props.players && playersMap && ( + + + !player.retired) + .map((player) => renderPlayerOption(player)) + } + search={(options, value): DropdownItemProps[] => { + return options.filter((option) => { + const player = JSON.parse(option.value as string); + return player.name.toLowerCase().includes(value.toLowerCase()); + }); + }} + defaultValue={ + props.editingPlayer + ? JSON.stringify( + playersMap.get((props.editingPlayer as DbDoublesPair)["player1_id"]) + ) + : undefined + } + placeholder={"Average QuickHit user"} + onChange={(_, data): void => { + const user = JSON.parse(data.value as string); + setFirstPlayer(user); + }} + /> + + + !player.retired) + .map((player) => renderPlayerOption(player)) + } + search={(options, value): DropdownItemProps[] => { + return options.filter((option) => { + const player = JSON.parse(option.value as string); + return player.name.toLowerCase().includes(value.toLowerCase()); + }); + }} + defaultValue={ + props.editingPlayer + ? JSON.stringify( + playersMap.get((props.editingPlayer as DbDoublesPair)["player2_id"]) + ) + : undefined + } + placeholder={"Average QuickHit user"} + onChange={(_, data): void => { + const user = JSON.parse(data.value as string); + setSecondPlayer(user); + }} + /> + + + )} setName(data.value)} @@ -157,7 +256,7 @@ function NewEditPlayer(props: NewEditPlayerProps): JSX.Element { /> { render( void; + doublesOnly?: boolean; } /** @@ -41,7 +42,16 @@ function NewGame(props: NewGameStoreProps & NewGameOwnProps & TTRefreshDispatchT const [losingPlayerScore, setLosingPlayerScore] = React.useState(); const [availablePlayers, setAvailablePlayers] = React.useState(props.players); - useEffect(() => setAvailablePlayers(props.players.filter((player) => !player.retired)), [props.players]); + useEffect(() => { + const availablePlayers: DbPlayer[] = []; + + if (!props.doublesOnly) { + availablePlayers.push(...props.players.filter((player) => !player.retired)); + } + + availablePlayers.push(...(props.doublesPairs.filter((doublesPair) => !doublesPair.retired) as DbPlayer[])); + setAvailablePlayers(availablePlayers); + }, [props.players, props.doublesPairs]); const sendCreateRequest = (addAnother: boolean): void => { const onSuccess = (): void => { @@ -69,70 +79,79 @@ function NewGame(props: NewGameStoreProps & NewGameOwnProps & TTRefreshDispatchT // Getting the latest players from the DB to ensure up to date ELO. QuickHitAPI.getPlayers((players: DbPlayer[]) => { - const playersMap = getPlayersMap(players); + QuickHitAPI.getDoublesPairs((doublesPairs: DbDoublesPair[]) => { + const playersMap = getPlayersMap(players, doublesPairs); - let kFactor = 15; - let happyHour = false; + let kFactor = 15; + let happyHour = false; - // If it is currently a happy hour. - if ( - new Date().getHours() >= props.happyHour.hourStart && - new Date().getHours() <= props.happyHour.hourStart + 1 - ) { - kFactor = 15 * props.happyHour.multiplier; - happyHour = true; - } + // If it is currently a happy hour. + if ( + new Date().getHours() >= props.happyHour.hourStart && + new Date().getHours() <= props.happyHour.hourStart + 1 + ) { + kFactor = 15 * props.happyHour.multiplier; + happyHour = true; + } - const elo = new EloRank(kFactor); - const winnerElo = playersMap.get(winningPlayer.id)?.elo; - const loserElo = playersMap.get(losingPlayer.id)?.elo; + const elo = new EloRank(kFactor); + const winnerElo = playersMap.get(winningPlayer.id)?.elo; + const loserElo = playersMap.get(losingPlayer.id)?.elo; - if (!(winnerElo && loserElo)) { - onError("Could not get latest player data!"); - return; - } + if (!(winnerElo && loserElo)) { + onError("Could not get latest player data!"); + return; + } - const winningPlayerUnderPlacement = isUnderPlacement( - getWinLossForPlayer(winningPlayer.id, props.matches).matches - ); - const losingPlayerUnderPlacement = isUnderPlacement( - getWinLossForPlayer(losingPlayer.id, props.matches).matches - ); + const winningPlayerUnderPlacement = isUnderPlacement( + getWinLossForPlayerOrPair(winningPlayer.id, props.matches).matches + ); + const losingPlayerUnderPlacement = isUnderPlacement( + getWinLossForPlayerOrPair(losingPlayer.id, props.matches).matches + ); - // Gets expected score for first parameter - const winningPlayerExpectedScore = elo.getExpected(winnerElo, loserElo); - const losingPlayerExpectedScore = elo.getExpected(loserElo, winnerElo); + // Gets expected score for first parameter + const winningPlayerExpectedScore = elo.getExpected(winnerElo, loserElo); + const losingPlayerExpectedScore = elo.getExpected(loserElo, winnerElo); - // update score, 1 if won 0 if lost - let winnerNewElo = elo.updateRating(winningPlayerExpectedScore, 1, winnerElo); - let loserNewElo = elo.updateRating(losingPlayerExpectedScore, 0, loserElo); + // update score, 1 if won 0 if lost + let winnerNewElo = elo.updateRating(winningPlayerExpectedScore, 1, winnerElo); + let loserNewElo = elo.updateRating(losingPlayerExpectedScore, 0, loserElo); - if (winningPlayerUnderPlacement) { - winnerNewElo = Math.ceil(winnerNewElo * 1.05); - } + if (winningPlayerUnderPlacement) { + winnerNewElo = Math.ceil(winnerNewElo * 1.05); + } - if (losingPlayerUnderPlacement) { - loserNewElo = Math.ceil(loserNewElo * 1.05); - } + if (losingPlayerUnderPlacement) { + loserNewElo = Math.ceil(loserNewElo * 1.05); + } - const matchToAdd: DbMatch = { - id: uuidv4(), - date: new Date().toISOString(), - winning_player_id: winningPlayer.id, - winning_player_score: winningPlayerScore, - winning_player_original_elo: winnerElo, - losing_player_id: losingPlayer.id, - losing_player_score: losingPlayerScore, - losing_player_original_elo: loserElo, - winner_new_elo: winnerNewElo, - loser_new_elo: loserNewElo, - happy_hour: happyHour, - }; - // Assigning new elo values to player object, then PATCHING. - winningPlayer.elo = winnerNewElo; - losingPlayer.elo = loserNewElo; + const matchToAdd: DbMatch = { + id: uuidv4(), + date: new Date().toISOString(), + winning_player_id: winningPlayer.id, + winning_player_score: winningPlayerScore, + winning_player_original_elo: winnerElo, + losing_player_id: losingPlayer.id, + losing_player_score: losingPlayerScore, + losing_player_original_elo: loserElo, + winner_new_elo: winnerNewElo, + loser_new_elo: loserNewElo, + happy_hour: happyHour, + }; + // Assigning new elo values to player object, then PATCHING. + winningPlayer.elo = winnerNewElo; + losingPlayer.elo = loserNewElo; - QuickHitAPI.addNewMatch(matchToAdd, winningPlayer, losingPlayer, onSuccess, onError); + QuickHitAPI.addNewMatch( + matchToAdd, + winningPlayer, + losingPlayer, + props.doublesOnly ?? false, + onSuccess, + onError + ); + }, onError); }, onError); }; @@ -192,7 +211,7 @@ function NewGame(props: NewGameStoreProps & NewGameOwnProps & TTRefreshDispatchT fluid label={ - Winning player + Winner } @@ -209,7 +228,7 @@ function NewGame(props: NewGameStoreProps & NewGameOwnProps & TTRefreshDispatchT value={winningPlayer ? renderPlayerOption(winningPlayer).value : ""} /> - + - Losing player + Loser } @@ -243,7 +262,7 @@ function NewGame(props: NewGameStoreProps & NewGameOwnProps & TTRefreshDispatchT value={losingPlayer ? renderPlayerOption(losingPlayer).value : ""} /> - + , TTDataPropsTypeCombined {} function PlayerStatistics(props: PlayerStatisticsProps): JSX.Element { - const playersMap = getPlayersMap(props.players); + const playersMap = getPlayersMap(props.players, props.doublesPairs); const player = playersMap.get(props.match.params.playerId); const extraStats: ExtraPlayerStats = player ? getExtraPlayerStats(player.id, props.matches) @@ -45,10 +45,12 @@ function PlayerStatistics(props: PlayerStatisticsProps): JSX.Element { {player.name} } onRequestMade={(): void => props.setForceRefresh(true)} + players={props.players} />
@@ -107,7 +109,9 @@ function PlayerStatistics(props: PlayerStatisticsProps): JSX.Element { ) : ( ) : ( { + const onSuccess = (doublesPairs: DbDoublesPair[]): void => { + props.setDoublesPairs(doublesPairs); + }; + + const onFailure = (error: string): void => { + makeErrorToast("Could not get doubles pairs", error); + props.setLoading(false); + }; + + QuickHitAPI.getDoublesPairs(onSuccess, onFailure); + }; + const getBadges = (): void => { const onSuccess = (badges: DbBadge[]): void => { props.setBadges(badges); @@ -140,6 +154,7 @@ function QHDataLoader(props: QHDataLoaderProps): JSX.Element { getMatches(); getTournaments(); getPlayers(); + getDoublesPairs(); }; useEffect(() => { @@ -180,7 +195,7 @@ function QHDataLoader(props: QHDataLoaderProps): JSX.Element { ); } -export const getWinLossForPlayer = (playerId: string, matches: DbMatch[]): WinLoss => { +export const getWinLossForPlayerOrPair = (playerId: string, matches: DbMatch[]): WinLoss => { const winLoss: WinLoss = { wins: 0, losses: 0, @@ -356,8 +371,8 @@ export const getExtraPlayerStats = (playerId: string, matches: DbMatch[]): Extra return { wins, losses, formGuide, minELO, maxELO, victim, nemesis }; }; -export const getPlayersMap = (players: DbPlayer[]): Map => { - const playersMap: Map = new Map(); +export const getPlayersMap = (players: DbPlayer[], doublesPairs?: DbDoublesPair[]): Map => { + const playersMap: Map = new Map(); if (players) { players.forEach((player) => { @@ -365,6 +380,12 @@ export const getPlayersMap = (players: DbPlayer[]): Map => { }); } + if (doublesPairs) { + doublesPairs.forEach((doublesPair) => { + playersMap.set(doublesPair.id, doublesPair); + }); + } + return playersMap; }; diff --git a/src/components/RecentGames/RecentGames.tsx b/src/components/RecentGames/RecentGames.tsx index c8b31c5..57f633b 100644 --- a/src/components/RecentGames/RecentGames.tsx +++ b/src/components/RecentGames/RecentGames.tsx @@ -23,7 +23,7 @@ import { import { FeedEventProps } from "semantic-ui-react/dist/commonjs/views/Feed/FeedEvent"; import ReactTimeAgo from "react-time-ago"; import { TTDataPropsTypeCombined } from "../../containers/shared"; -import { getPlayersMap, getWinLossForPlayer } from "../QHDataLoader/QHDataLoader"; +import { getPlayersMap, getWinLossForPlayerOrPair } from "../QHDataLoader/QHDataLoader"; import { DbMatch, DbMatchComment, DbMatchReaction, DbPlayer, isUnderPlacement } from "../../types/database/models"; import RecentGamesStatistics from "./RecentGamesStatistics/RecentGamesStatistics"; import EmojiPicker, { SKIN_TONE_NEUTRAL } from "emoji-picker-react"; @@ -215,8 +215,8 @@ export const turnMatchIntoFeedItems = ( break; } - const winningPlayerWinLoss = getWinLossForPlayer(match.winning_player_id, allMatches); - const losingPlayerWinLoss = getWinLossForPlayer(match.losing_player_id, allMatches); + const winningPlayerWinLoss = getWinLossForPlayerOrPair(match.winning_player_id, allMatches); + const losingPlayerWinLoss = getWinLossForPlayerOrPair(match.losing_player_id, allMatches); const winningPlayerUnranked = isUnderPlacement(winningPlayerWinLoss.matches); const losingPlayerUnranked = isUnderPlacement(losingPlayerWinLoss.matches); diff --git a/src/components/Tournament/Tournament.tsx b/src/components/Tournament/Tournament.tsx index b1de6e6..f314805 100644 --- a/src/components/Tournament/Tournament.tsx +++ b/src/components/Tournament/Tournament.tsx @@ -657,6 +657,9 @@ function Tournament(props: TournamentReduxProps & TTRefreshDispatchType): JSX.El return (
+
+ +
{sortedTournaments.length > 0 && tournamentIsFinished(sortedTournaments[0]) ? (
Congratulations {getWinner(sortedTournaments[0])}!
diff --git a/src/containers/AchievementFeed/AchievementFeed.ts b/src/containers/AchievementFeed/AchievementFeed.ts index 87c4cc6..732ae4b 100644 --- a/src/containers/AchievementFeed/AchievementFeed.ts +++ b/src/containers/AchievementFeed/AchievementFeed.ts @@ -21,6 +21,7 @@ export function mapStateToProps( happyHour: store.ttData.happyHour, badges: store.ttData.badges, tournaments: store.ttData.tournaments, + doublesPairs: store.ttData.doublesPairs, focusedPlayerId: ownProps.focusedPlayerId, }; } diff --git a/src/containers/HallOfFallen/HallOfFallen.ts b/src/containers/HallOfFallen/HallOfFallen.ts index 446245a..7d72b8f 100644 --- a/src/containers/HallOfFallen/HallOfFallen.ts +++ b/src/containers/HallOfFallen/HallOfFallen.ts @@ -16,6 +16,7 @@ export function mapStateToProps(store: QuickHitReduxStores): HallOfFallenReduxPr happyHour: store.ttData.happyHour, badges: store.ttData.badges, tournaments: store.ttData.tournaments, + doublesPairs: store.ttData.doublesPairs, disableMusic: store.viewStore.disableMusic, chosenInstance: store.authStore.chosenInstance, }; diff --git a/src/containers/Ladder/Ladder.ts b/src/containers/Ladder/Ladder.ts index cc16172..13f1826 100644 --- a/src/containers/Ladder/Ladder.ts +++ b/src/containers/Ladder/Ladder.ts @@ -22,6 +22,7 @@ export function mapStateToProps(store: QuickHitReduxStores): TTStoreState & View happyHour: store.ttData.happyHour, badges: store.ttData.badges, tournaments: store.ttData.tournaments, + doublesPairs: store.ttData.doublesPairs, hideUnplacedPlayers: store.viewStore.hideUnplacedPlayers, showCards: store.viewStore.showCards, disableMusic: store.viewStore.disableMusic, diff --git a/src/containers/NewGame/NewGame.ts b/src/containers/NewGame/NewGame.ts index 8ba9c5f..bce07ea 100644 --- a/src/containers/NewGame/NewGame.ts +++ b/src/containers/NewGame/NewGame.ts @@ -2,10 +2,11 @@ import { connect } from "react-redux"; import { mapTTDispatchToProps } from "../shared"; import NewGame, { NewGameOwnProps } from "../../components/NewGame/NewGame"; import { QuickHitReduxStores } from "../../redux/types/store"; -import { DbHappyHour, DbMatch, DbPlayer } from "../../types/database/models"; +import { DbDoublesPair, DbHappyHour, DbMatch, DbPlayer } from "../../types/database/models"; export interface NewGameStoreProps { players: DbPlayer[]; + doublesPairs: DbDoublesPair[]; happyHour: DbHappyHour; matches: DbMatch[]; } @@ -16,6 +17,7 @@ export function mapStateToProps( ): NewGameStoreProps & NewGameOwnProps { return { players: store.ttData.players, + doublesPairs: store.ttData.doublesPairs, happyHour: store.ttData.happyHour, matches: store.ttData.matches, customModalOpenElement: ownProps.customModalOpenElement, diff --git a/src/containers/QHDataLoader/QHDataLoader.ts b/src/containers/QHDataLoader/QHDataLoader.ts index c94b02d..0e5157f 100644 --- a/src/containers/QHDataLoader/QHDataLoader.ts +++ b/src/containers/QHDataLoader/QHDataLoader.ts @@ -2,7 +2,7 @@ import { Dispatch } from "redux"; import * as actions from "../../redux/actions/TTActions"; import { connect } from "react-redux"; import QHDataLoader from "../../components/QHDataLoader/QHDataLoader"; -import { DbBadge, DbHappyHour, DbMatch, DbPlayer, DbTournament } from "../../types/database/models"; +import { DbBadge, DbDoublesPair, DbHappyHour, DbMatch, DbPlayer, DbTournament } from "../../types/database/models"; import { mapTTDataToProps, TTRefreshDispatchType } from "../shared"; export interface DataLoaderDispatchType extends TTRefreshDispatchType { @@ -12,6 +12,7 @@ export interface DataLoaderDispatchType extends TTRefreshDispatchType { setLoading: (newLoading: boolean) => void; setBadges: (newBadges: DbBadge[]) => void; setTournaments: (newTournaments: DbTournament[]) => void; + setDoublesPairs: (newDoublesPairs: DbDoublesPair[]) => void; } export function mapDispatchToProps( @@ -23,6 +24,7 @@ export function mapDispatchToProps( | actions.SetHappyHourAction | actions.SetBadgesAction | actions.SetTournamentsAction + | actions.SetDoublesPairsAction > ): DataLoaderDispatchType { return { @@ -36,6 +38,8 @@ export function mapDispatchToProps( setBadges: (newBadges: DbBadge[]): actions.SetBadgesAction => dispatch(actions.setBadges(newBadges)), setTournaments: (newTournaments: DbTournament[]): actions.SetTournamentsAction => dispatch(actions.setTournaments(newTournaments)), + setDoublesPairs: (newDoublesPairs: DbDoublesPair[]): actions.SetDoublesPairsAction => + dispatch(actions.setDoublesPairs(newDoublesPairs)), }; } diff --git a/src/containers/RecentGames/RecentGames.ts b/src/containers/RecentGames/RecentGames.ts index 2665463..9336199 100644 --- a/src/containers/RecentGames/RecentGames.ts +++ b/src/containers/RecentGames/RecentGames.ts @@ -24,6 +24,7 @@ export function mapStateToProps( happyHour: store.ttData.happyHour, badges: store.ttData.badges, tournaments: store.ttData.tournaments, + doublesPairs: store.ttData.doublesPairs, currentUser: store.viewStore.currentUser, focusedPlayerId: ownProps.focusedPlayerId, }; diff --git a/src/containers/Tournament/Tournament.ts b/src/containers/Tournament/Tournament.ts index 9c82c58..30ab27f 100644 --- a/src/containers/Tournament/Tournament.ts +++ b/src/containers/Tournament/Tournament.ts @@ -15,6 +15,7 @@ export function mapStateToProps(store: QuickHitReduxStores): TournamentReduxProp refresh: store.ttData.refresh, happyHour: store.ttData.happyHour, badges: store.ttData.badges, + doublesPairs: store.ttData.doublesPairs, tournaments: store.ttData.tournaments, disableMusic: store.viewStore.disableMusic, chosenInstance: store.authStore.chosenInstance, diff --git a/src/containers/shared/index.ts b/src/containers/shared/index.ts index eab449b..9aa72f5 100644 --- a/src/containers/shared/index.ts +++ b/src/containers/shared/index.ts @@ -14,6 +14,7 @@ export function mapTTDataToProps(store: QuickHitReduxStores): TTStoreState & { c return { loading: store.ttData.loading, players: store.ttData.players, + doublesPairs: store.ttData.doublesPairs, matches: store.ttData.matches, happyHour: store.ttData.happyHour, badges: store.ttData.badges, diff --git a/src/redux/actions/TTActions/index.ts b/src/redux/actions/TTActions/index.ts index 9d5bea1..943147b 100644 --- a/src/redux/actions/TTActions/index.ts +++ b/src/redux/actions/TTActions/index.ts @@ -1,5 +1,5 @@ import * as constants from "../../constants/TTConstants"; -import { DbBadge, DbHappyHour, DbMatch, DbPlayer, DbTournament } from "../../../types/database/models"; +import { DbBadge, DbDoublesPair, DbHappyHour, DbMatch, DbPlayer, DbTournament } from "../../../types/database/models"; export interface SetMatchesAction { type: constants.SET_MATCHES_TYPE; @@ -36,6 +36,11 @@ export interface SetTournamentsAction { value: DbTournament[]; } +export interface SetDoublesPairsAction { + type: constants.SET_DOUBLES_PAIRS_TYPE; + value: DbDoublesPair[]; +} + export function setMatches(newMatches: DbMatch[]): SetMatchesAction { return { type: constants.SET_MATCHES, @@ -84,3 +89,10 @@ export function setTournaments(newTournaments: DbTournament[]): SetTournamentsAc value: newTournaments, }; } + +export function setDoublesPairs(newDoublesPairs: DbDoublesPair[]): SetDoublesPairsAction { + return { + type: constants.SET_DOUBLES_PAIRS, + value: newDoublesPairs, + }; +} diff --git a/src/redux/constants/TTConstants/index.ts b/src/redux/constants/TTConstants/index.ts index b3c004d..2af95b3 100644 --- a/src/redux/constants/TTConstants/index.ts +++ b/src/redux/constants/TTConstants/index.ts @@ -18,3 +18,6 @@ export type SET_BADGES_TYPE = typeof SET_BADGES; export const SET_TOURNAMENTS = "SET_TOURNAMENTS"; export type SET_TOURNAMENTS_TYPE = typeof SET_TOURNAMENTS; + +export const SET_DOUBLES_PAIRS = "SET_DOUBLES_PAIRS"; +export type SET_DOUBLES_PAIRS_TYPE = typeof SET_DOUBLES_PAIRS; diff --git a/src/redux/reducers/TTReducer/index.ts b/src/redux/reducers/TTReducer/index.ts index 1025deb..8b79b23 100644 --- a/src/redux/reducers/TTReducer/index.ts +++ b/src/redux/reducers/TTReducer/index.ts @@ -1,6 +1,7 @@ import { TTStoreState } from "../../types/TTTypes"; import { SET_BADGES, + SET_DOUBLES_PAIRS, SET_FORCE_REFRESH, SET_HAPPY_HOUR, SET_LOADING, @@ -10,6 +11,7 @@ import { } from "../../constants/TTConstants"; import { SetBadgesAction, + SetDoublesPairsAction, SetForceRefreshAction, SetHappyHourAction, SetLoadingAction, @@ -29,6 +31,7 @@ export const dataInitialState: TTStoreState = { }, badges: [], tournaments: [], + doublesPairs: [], refresh: false, }; @@ -42,6 +45,7 @@ export function ttReducer( | SetHappyHourAction | SetBadgesAction | SetTournamentsAction + | SetDoublesPairsAction ): TTStoreState { switch (action.type) { case SET_MATCHES: @@ -58,6 +62,8 @@ export function ttReducer( return { ...state, badges: action.value }; case SET_TOURNAMENTS: return { ...state, tournaments: action.value }; + case SET_DOUBLES_PAIRS: + return { ...state, doublesPairs: action.value }; default: return state; } diff --git a/src/redux/types/TTTypes/index.ts b/src/redux/types/TTTypes/index.ts index ca7cf0d..90bf48c 100644 --- a/src/redux/types/TTTypes/index.ts +++ b/src/redux/types/TTTypes/index.ts @@ -1,4 +1,4 @@ -import { DbBadge, DbHappyHour, DbMatch, DbPlayer, DbTournament } from "../../../types/database/models"; +import { DbBadge, DbDoublesPair, DbHappyHour, DbMatch, DbPlayer, DbTournament } from "../../../types/database/models"; export interface TTStoreState { matches: DbMatch[]; @@ -6,6 +6,7 @@ export interface TTStoreState { happyHour: DbHappyHour; badges: DbBadge[]; tournaments: DbTournament[]; + doublesPairs: DbDoublesPair[]; refresh: boolean; loading: boolean; } diff --git a/src/types/database/models.ts b/src/types/database/models.ts index 739074c..0e3fce3 100644 --- a/src/types/database/models.ts +++ b/src/types/database/models.ts @@ -36,6 +36,11 @@ export interface DbPlayer { retired?: boolean; } +export interface DbDoublesPair extends DbPlayer { + player1_id: string; + player2_id: string; +} + export interface DbHappyHour { /* yyyy-mm-dd */ date: string; diff --git a/src/util/QuickHitPage.ts b/src/util/QuickHitPage.ts index 55fd932..9a114d0 100644 --- a/src/util/QuickHitPage.ts +++ b/src/util/QuickHitPage.ts @@ -8,6 +8,7 @@ export function BASE_PATH(): string { export enum QuickHitPage { HOME = "/", LADDER = "/ladder", + DOUBLES_LADDER = "/ladder/doubles", RECENT_GAMES = "/recent", STATISTICS = "/player/:playerId", TOURNAMENT = "/tournament",