From eb9f3ff9efcd4c0a6cff8a95f942f0fcc6032ab8 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 20:20:55 +0530 Subject: [PATCH 01/21] feat(client/race): change player to spectator on auth token change while racing --- apps/client/src/i18n/en/race.json | 4 ++++ apps/client/src/pages/race/Race.page.tsx | 26 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/apps/client/src/i18n/en/race.json b/apps/client/src/i18n/en/race.json index 33460173..eb77756d 100644 --- a/apps/client/src/i18n/en/race.json +++ b/apps/client/src/i18n/en/race.json @@ -4,6 +4,10 @@ "race_start": { "title": "Ready, set, go!", "message": "{{player_name}} started the race. Good luck!" + }, + "reconnected_as_new": { + "title": "Reconnected as a New Player", + "message": "Unfortunately, you were removed from the race after an extended disconnection. Despite your reconnection, the server had to remove you. You'll now be treated as a new player in the tournament." } } } diff --git a/apps/client/src/pages/race/Race.page.tsx b/apps/client/src/pages/race/Race.page.tsx index 7ef8785d..aa23dcd7 100644 --- a/apps/client/src/pages/race/Race.page.tsx +++ b/apps/client/src/pages/race/Race.page.tsx @@ -44,6 +44,32 @@ export function Race(): ReactElement { const selfPlayerId = useRef(savedData.savedPlayerId); const [isTypeLocked, setIsTypeLocked] = useState(true); const [isSpectator, setIsSpectator] = useState(false); + const prevAuthToken = useRef(savedData.authToken); + + useEffect(() => { + const handleAuthTokenChange = (): void => + savedData.addEventListener(() => { + const authToken = savedData.authToken; + if (prevAuthToken.current === null || authToken === null) { + return; + } + + if (prevAuthToken.current !== authToken) { + prevAuthToken.current = authToken; + setIsSpectator(true); + } + addToast({ + title: t('toasts.reconnected_as_new.title'), + type: ToastType.Info, + message: t('toasts.reconnected_as_new.message') as string, + }); + }); + handleAuthTokenChange(); + + return () => { + savedData.removeEventListener(handleAuthTokenChange); + }; + }, []); // If the tournament state changed to leaderboard navigate to the leaderboard page. useEffect((): void => { From e7f62f1831db08164567177bef61d14c6c37925b Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 20:29:52 +0530 Subject: [PATCH 02/21] feat(client/home): add toast for invalid tournament id --- apps/client/src/i18n/en/home.json | 6 ++++++ apps/client/src/pages/home/Home.page.tsx | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/client/src/i18n/en/home.json b/apps/client/src/i18n/en/home.json index 29e27f04..5775e5d7 100644 --- a/apps/client/src/i18n/en/home.json +++ b/apps/client/src/i18n/en/home.json @@ -17,6 +17,12 @@ } ] }, + "toasts": { + "invalid_room_id": { + "title": "Invalid room id", + "message": "The room id you entered is invalid. Please try again." + } + }, "room_id": "(Room id: {{id}})", "actions": { "join": "Join", diff --git a/apps/client/src/pages/home/Home.page.tsx b/apps/client/src/pages/home/Home.page.tsx index bb931aa7..b53f5397 100644 --- a/apps/client/src/pages/home/Home.page.tsx +++ b/apps/client/src/pages/home/Home.page.tsx @@ -7,6 +7,7 @@ import { generateAvatarLink } from '@razor/util'; import cs from 'classnames'; import { debounce } from 'lodash'; import { ReactComponent as ChevronRight } from 'pixelarticons/svg/chevron-right.svg'; +import { ReactComponent as TagIcon } from 'pixelarticons/svg/label.svg'; import { ReactComponent as Logo } from '../../assets/images/logo.svg'; import { ReactComponent as LogoFill } from '../../assets/images/logo-fill.svg'; @@ -22,7 +23,9 @@ import { Link, Panel, Text, + ToastType, } from '../../components'; +import { useToastContext } from '../../hooks'; import { TextSize, TextType } from '../../models'; import { endSocket } from '../../services'; import { @@ -33,6 +36,7 @@ import { export function Home(): ReactElement { const { t } = useTranslation('home'); const { roomId } = useParams(); + const addToast = useToastContext(); // disconnect any socket connection if user navigates back to home page. useEffect(() => { @@ -142,8 +146,12 @@ export function Home(): ReactElement { if (isIdValid) { navigate(`/${value}`); } else { - // TODO: Implement proper component - alert('Invalid tournament id'); + addToast({ + title: t('toasts.invalid_room_id.title'), + type: ToastType.Error, + message: t('toasts.invalid_room_id.message') as string, + icon: , + }); } }; From c58b6f81b1c504abaea2c075248910dd2e37db58 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 20:34:00 +0530 Subject: [PATCH 03/21] feat(client): use css mask to fade overflow in scrolls --- .../organisms/panel/Panel.component.tsx | 31 +++++++------- .../player-list/PlayerList.template.tsx | 41 ++++++------------- apps/client/src/styles.css | 27 ++++++++++++ 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/apps/client/src/components/organisms/panel/Panel.component.tsx b/apps/client/src/components/organisms/panel/Panel.component.tsx index 89d2c6ca..02c50b37 100644 --- a/apps/client/src/components/organisms/panel/Panel.component.tsx +++ b/apps/client/src/components/organisms/panel/Panel.component.tsx @@ -33,12 +33,11 @@ export function Panel({ title, children }: PanelProps): ReactElement { return (
-
+
- {isCollapse ? ( - - Show Panel - - ) : ( - - Hide Panel - - )} + + {isCollapse ? 'Show Panel' : 'Hide Panel'} +
diff --git a/apps/client/src/pages/room/templates/player-list/PlayerList.template.tsx b/apps/client/src/pages/room/templates/player-list/PlayerList.template.tsx index 60290653..d567b249 100644 --- a/apps/client/src/pages/room/templates/player-list/PlayerList.template.tsx +++ b/apps/client/src/pages/room/templates/player-list/PlayerList.template.tsx @@ -23,8 +23,8 @@ export function PlayerList({ ); const playersModel = useRef(game.playersModel); const containerRef = useRef(null); - const [topCoverVisible, setTopCoverVisible] = useState(false); - const [bottomCoverVisible, setBottomCoverVisible] = useState(true); + const [isTopOverflowing, setTopOverflowing] = useState(false); + const [isBottomOverflowing, setBottomOverflowing] = useState(true); const [hasOverflow, setHasOverflow] = useState(false); useEffect(() => { @@ -40,12 +40,12 @@ export function PlayerList({ const scrollPosition = element.scrollHeight - element.offsetHeight; if (element.scrollTop === 0) { - setTopCoverVisible(false); + setTopOverflowing(false); } else if (element.scrollTop === scrollPosition) { - setBottomCoverVisible(false); + setBottomOverflowing(false); } else { - setTopCoverVisible(true); - setBottomCoverVisible(true); + setTopOverflowing(true); + setBottomOverflowing(true); } }; @@ -57,10 +57,10 @@ export function PlayerList({ const scrollPosition = element.scrollHeight - element.offsetHeight; if (scrollPosition <= 0) { - setBottomCoverVisible(false); + setBottomOverflowing(false); setHasOverflow(false); } else { - setBottomCoverVisible(true); + setBottomOverflowing(true); setHasOverflow(true); } @@ -105,19 +105,12 @@ export function PlayerList({
-
) : null}
-
); diff --git a/apps/client/src/styles.css b/apps/client/src/styles.css index ddc2aced..9a5c302c 100644 --- a/apps/client/src/styles.css +++ b/apps/client/src/styles.css @@ -53,3 +53,30 @@ canvas { svg#car { shape-rendering: crispEdges; } + +.overflow-mask { + mask-image: linear-gradient( + to bottom, + transparent 0, + theme('colors.surface') var(--top-mask-size, 0px), + theme('colors.surface') calc(100% - var(--bottom-mask-size, 0px)), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0, + theme('colors.surface') var(--top-mask-size, 0px), + theme('colors.surface') calc(100% - var(--bottom-mask-size, 0px)), + transparent 100% + ); + --top-mask-size: 0px; + --bottom-mask-size: 0px; +} + +.top-overflowing { + --top-mask-size: 48px; +} + +.bottom-overflowing { + --bottom-mask-size: 48px; +} From 2236e91b0adc870f2cf1e64f1481eee3f3f30dd9 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 20:35:53 +0530 Subject: [PATCH 04/21] feat(client): improve ui --- .../src/pages/leaderboard/Leaderboard.page.tsx | 5 +++-- .../LeaderboardList.template.tsx | 18 ++++++++++++++++-- apps/client/src/pages/room/Room.page.tsx | 17 ++++++++++------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/client/src/pages/leaderboard/Leaderboard.page.tsx b/apps/client/src/pages/leaderboard/Leaderboard.page.tsx index 66b78acb..ff62e68d 100644 --- a/apps/client/src/pages/leaderboard/Leaderboard.page.tsx +++ b/apps/client/src/pages/leaderboard/Leaderboard.page.tsx @@ -61,11 +61,12 @@ export function Leaderboard(): ReactElement { image={panelImages[1]}> {t('panel.descriptions.1.content') as string} - {t('panel.descriptions.2.content') as string} - + */}
store.game); const entries: AppLeaderboard = game.leaderboardsModel[raceId] || []; const completedEntries = entries.filter( @@ -114,12 +119,20 @@ export function LeaderboardList({ imgURL={player.avatarLink} number={index + 1} rightText={`${values.wpm.toFixed(2)} wpm`} + isHighlighted={entry.playerId === savedData.savedPlayerId} key={entry.playerId} /> ); })} {completedEntries?.length > 0 && timeoutEntries?.length > 0 ? ( -
+
+ + {t('timeout_players') as string} + +
) : null} {timeoutEntries.map(entry => { const player = racePlayers[entry.playerId]; @@ -138,6 +151,7 @@ export function LeaderboardList({ title={player.name} imgURL={player.avatarLink} rightText={`${completePercentage}%`} + isHighlighted={entry.playerId === savedData.savedPlayerId} isTranslucent key={entry.playerId} /> diff --git a/apps/client/src/pages/room/Room.page.tsx b/apps/client/src/pages/room/Room.page.tsx index e38108dc..80c3d88b 100644 --- a/apps/client/src/pages/room/Room.page.tsx +++ b/apps/client/src/pages/room/Room.page.tsx @@ -116,20 +116,19 @@ export function Room(): ReactElement {
-
+
-
+
-
+
} From 7cc8dd1f8f61de229fd5a5f563765539e39b1987 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 20:46:49 +0530 Subject: [PATCH 05/21] feat(debugger): add connection logs and socket id to debugger --- apps/client/src/utils/Debugger.tsx | 74 +++++++++++++++++++++-- apps/client/src/utils/save-player-data.ts | 3 + 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/apps/client/src/utils/Debugger.tsx b/apps/client/src/utils/Debugger.tsx index 9a77a5be..a846ca2f 100644 --- a/apps/client/src/utils/Debugger.tsx +++ b/apps/client/src/utils/Debugger.tsx @@ -1,6 +1,8 @@ import { ReactElement, useEffect, useState } from 'react'; import cs from 'classnames'; +import { socket, tryReconnect } from '../services'; + import { savedData } from './save-player-data'; enum DebuggerCommands { @@ -12,6 +14,7 @@ export function Debugger(): ReactElement { const [typedText, setTypedText] = useState(''); const [isExpanded, toggleExpand] = useState(false); const [lastUpdatedTime, setLastUpdatedTime] = useState(0); + const [connectionLogs, setConnectionLogs] = useState([]); const alphabet = 'abcdefghijklmnopqrstuvwxyz'; const validCommands: string[] = [DebuggerCommands.ENABLE]; @@ -50,15 +53,38 @@ export function Debugger(): ReactElement { const updateData = (): void => { setLastUpdatedTime(Date.now()); - console.log('Updated data'); + console.log('Updated saved data'); + }; + + const getFormattedTime = (): string => { + const time = new Date(Date.now()); + return `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`; + }; + + const addConnectedLog = (): void => { + setConnectionLogs(prev => [ + ...prev, + `Connected @${getFormattedTime()}`, + savedData.authToken ? `AuthToken: ${savedData.authToken}` : '', + ]); + }; + const addDisconnectedLog = (reason: string): void => { + setConnectionLogs(prev => [ + ...prev, + `[${reason}] Disconnected @${getFormattedTime()}`, + ]); }; savedData.addEventListener(updateData); document.addEventListener('keydown', handleKeyDown); + socket.on('connect', addConnectedLog); + socket.on('disconnect', addDisconnectedLog); return () => { savedData.removeEventListener(updateData); document.removeEventListener('keydown', handleKeyDown); + socket.off('connect', addConnectedLog); + socket.off('disconnect', addDisconnectedLog); }; }, []); @@ -74,11 +100,14 @@ export function Debugger(): ReactElement { 'font-roboto text-sm text-right', 'overflow-hidden', 'transition-all duration-300', - isExpanded ? 'h-full' : 'h-10', + isExpanded ? 'h-full' : 'h-12', )}> -
+

Debugger

-
@@ -90,6 +119,43 @@ export function Debugger(): ReactElement { PlayerId: {savedData.savedPlayerId || 'N/A'} UserName: {savedData.savedPlayerName || 'N/A'} RoomId: {savedData.savedRoomId || 'N/A'} + SocketId: {savedData.savedSocketId || 'N/A'} + + {socket.connected ? ( + <> +
+ Connected + + ) : ( + <> +
+ Disconnected + + )} + + + + Connection Logs +
+ {connectionLogs.map((log, index) => ( + // eslint-disable-next-line react/no-array-index-key + {log} + ))} +
); diff --git a/apps/client/src/utils/save-player-data.ts b/apps/client/src/utils/save-player-data.ts index 22271fe3..18670970 100644 --- a/apps/client/src/utils/save-player-data.ts +++ b/apps/client/src/utils/save-player-data.ts @@ -5,9 +5,11 @@ import { AuthToken, PlayerId } from '@razor/models'; */ class SavedData { private _authToken: AuthToken | null = null; + // TODO: rename to playername public savedPlayerName: string | null = null; public savedPlayerId: PlayerId | null = null; public savedRoomId: string | null = null; + public savedSocketId: string | null = null; private listeners: (() => void)[] = []; public get authToken(): AuthToken | null { @@ -24,6 +26,7 @@ class SavedData { this.savedPlayerName = null; this.savedPlayerId = null; this.savedRoomId = null; + this.savedSocketId = null; this.runListeners(); } From 433fa5f617dd7e88f63911032e54dc9833c6b446 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 21:05:06 +0530 Subject: [PATCH 06/21] feat(race/timer): develop function to calculate recent wpm --- .../race-timer/utils/compute-recent-wpm.ts | 21 +++++++++++++++++++ libs/util/src/lib/generate-leaderboard.ts | 8 +++---- 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/pages/race/templates/race-timer/utils/compute-recent-wpm.ts diff --git a/apps/client/src/pages/race/templates/race-timer/utils/compute-recent-wpm.ts b/apps/client/src/pages/race/templates/race-timer/utils/compute-recent-wpm.ts new file mode 100644 index 00000000..550f3bdd --- /dev/null +++ b/apps/client/src/pages/race/templates/race-timer/utils/compute-recent-wpm.ts @@ -0,0 +1,21 @@ +import { AppPlayerLog } from '@razor/models'; +import { calculateWPM } from '@razor/util'; +import { RECENT_WPM_TIME_WINDOW } from 'apps/client/src/constants/race'; + +export function computeRecentWPM(playerLogs: AppPlayerLog[]): number { + // get logs for last RECENT_WPM_TIME_WINDOW seconds + const recentLogs = playerLogs.filter( + log => log.timestamp > Date.now() - RECENT_WPM_TIME_WINDOW * 1000, + ); + + if (recentLogs.length > 1) { + return calculateWPM( + recentLogs[0].timestamp, + Date.now(), + recentLogs[0].textLength, + recentLogs[recentLogs.length - 1].textLength, + ); + } else { + return 0; + } +} diff --git a/libs/util/src/lib/generate-leaderboard.ts b/libs/util/src/lib/generate-leaderboard.ts index 4f1be9f8..235cb251 100644 --- a/libs/util/src/lib/generate-leaderboard.ts +++ b/libs/util/src/lib/generate-leaderboard.ts @@ -51,7 +51,7 @@ export const generateLeaderboard = ( // Check whether the player has finished the race by comparing the last logged text length of the player and the race text length. if (playerLastTextLength === raceTextLength) { - const wpm = calculateWPM(raceTextLength, playerLogs[playerLogId]); + const wpm = calculateFullWPM(raceTextLength, playerLogs[playerLogId]); // Elapsed time = (Last timestamp - First timestamp) / 1000 <= In seconds const elapsedTime = (playerLogs[playerLogId][playerLogsLength - 1].timestamp - @@ -108,7 +108,7 @@ export const generateLeaderboard = ( * @param logs - Logs of a player. * @returns Average wpm. */ -const calculateWPM = (length: number, logs: AppPlayerLog[]): number => { +const calculateFullWPM = (length: number, logs: AppPlayerLog[]): number => { /** Placing markers(checkpoints) for race text length. This will partition race text length. */ const mark1 = Math.floor(length * 0.25); const mark2 = Math.floor(length * 0.5); @@ -171,7 +171,7 @@ const calculateWPM = (length: number, logs: AppPlayerLog[]): number => { // Calculate the quarter WPM using the difference between the two timestamp checkpoints and the two text length checkpoints. // * From example 1: // calculateQuarterWPM(1234567021, 1234567050, 119, 241) = 2.4 - quarterWPMs[index] = calculateQuarterWPM( + quarterWPMs[index] = calculateWPM( timestampCheckpoints[index], timestampCheckpoints[index + 1], textLengthCheckpoints[index], @@ -187,7 +187,7 @@ const calculateWPM = (length: number, logs: AppPlayerLog[]): number => { * * @returns WPM for specific quarter. */ -const calculateQuarterWPM = ( +export const calculateWPM = ( startTimestamp: number, endTimestamp: number, startTextLength: number, From 302798414ab4c0746d6545b1202be0e875d2a59b Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 21:15:00 +0530 Subject: [PATCH 07/21] feat(race/timer): add player's current wpm to timer --- .../molecules/timer/Timer.component.tsx | 64 ++++++++++++-- .../molecules/timer/Timer.stories.tsx | 3 + apps/client/src/constants/race.ts | 3 + apps/client/src/i18n/en/leaderboard.json | 3 +- apps/client/src/pages/race/Race.page.tsx | 16 ++-- .../race-timer/RaceTimer.template.tsx | 83 +++++++++++++++++++ 6 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 apps/client/src/pages/race/templates/race-timer/RaceTimer.template.tsx diff --git a/apps/client/src/components/molecules/timer/Timer.component.tsx b/apps/client/src/components/molecules/timer/Timer.component.tsx index ef4dbafb..1163bdbc 100644 --- a/apps/client/src/components/molecules/timer/Timer.component.tsx +++ b/apps/client/src/components/molecules/timer/Timer.component.tsx @@ -6,21 +6,30 @@ import { Text } from '../../atoms'; export interface TimerProps { time: number; // in seconds + showSpeed?: boolean; + speedValue?: string; + speedometerPercentage?: number; onTimeEnd?: () => void; } /** * * @param time - Time in seconds + * @param showSpeed - Whether to show speedometer or not + * @param speedValue - Speed value in wpm + * @param speedometerPercentage - Percentage of speedometer (0-1) * @param [onTimeEnd] - Callback function to be called when time ends (optional) */ export function Timer({ time, + showSpeed = false, + speedValue = '0 wpm', + speedometerPercentage = 0, // eslint-disable-next-line @typescript-eslint/no-empty-function onTimeEnd = (): void => {}, }: TimerProps): ReactElement { const startTimestamp = useRef(Date.now()); - const previousTimestamp = useRef(Date.now()); + const previousTimestamp = useRef(0); const [seconds, setSeconds] = useState(time); const [milliseconds, setMilliseconds] = useState(0); const circleIndicator = useRef(null); @@ -28,8 +37,8 @@ export function Timer({ useEffect(() => { startTimestamp.current = Date.now(); - previousTimestamp.current = Date.now(); - setSeconds(time); + previousTimestamp.current = 0; + setSeconds(Math.trunc(time)); setMilliseconds(0); timerEnded.current = false; }, [time]); @@ -51,6 +60,7 @@ export function Timer({ return; } + // TODO: Check possibility of removing previousTimestamp const now = Date.now(); // Optimizing for high refresh rate screens @@ -108,7 +118,7 @@ export function Timer({ 'flex justify-center items-center', 'relative', )}> -
+
{seconds.toString().padStart(2, '0')} @@ -119,16 +129,50 @@ export function Timer({ {`.${milliseconds.toString().padStart(2, '0')}`}
+ {showSpeed && ( +
+
+ + {speedValue || '0'} + +
+ )} + {showSpeed && ( + + )} + {/* Circle below time indicator */} + + + + +
); diff --git a/apps/client/src/components/molecules/timer/Timer.stories.tsx b/apps/client/src/components/molecules/timer/Timer.stories.tsx index b48707c9..22d1103b 100644 --- a/apps/client/src/components/molecules/timer/Timer.stories.tsx +++ b/apps/client/src/components/molecules/timer/Timer.stories.tsx @@ -7,6 +7,9 @@ export default { component: Timer, args: { time: 10, + showSpeed: false, + speedValue: '50 WPM', + speedometerPercentage: 0.25, onTimeEnd: () => { console.log('Time ended'); }, diff --git a/apps/client/src/constants/race.ts b/apps/client/src/constants/race.ts index 1cbe4e00..a9f3f51f 100644 --- a/apps/client/src/constants/race.ts +++ b/apps/client/src/constants/race.ts @@ -1,2 +1,5 @@ export const MAX_INVALID_CHARS_ALLOWED = 5; export const MIN_RACE_TRACKS = 5; + +export const RECENT_WPM_TIME_WINDOW = 5; // seconds +export const RECENT_WPM_UPDATE_INTERVAL = 1000; // milliseconds diff --git a/apps/client/src/i18n/en/leaderboard.json b/apps/client/src/i18n/en/leaderboard.json index 2059bd6e..39cd7a21 100644 --- a/apps/client/src/i18n/en/leaderboard.json +++ b/apps/client/src/i18n/en/leaderboard.json @@ -16,5 +16,6 @@ "content": "Want to show off your leaderboard achievements to your friends and followers on social media? Simply take a screenshot of the leaderboard, featuring your name and stats, and share it with the world. Let everyone see your typing prowess and inspire them to join the action on Razor!" } ] - } + }, + "timeout_players": "Timeout Players" } diff --git a/apps/client/src/pages/race/Race.page.tsx b/apps/client/src/pages/race/Race.page.tsx index aa23dcd7..45097d20 100644 --- a/apps/client/src/pages/race/Race.page.tsx +++ b/apps/client/src/pages/race/Race.page.tsx @@ -17,8 +17,7 @@ import { ReactComponent as EyeIcon } from 'pixelarticons/svg/eye.svg'; import { ReactComponent as Logo } from '../../assets/images/logo.svg'; import { Text, ToastType } from '../../components'; -import { Timer } from '../../components/molecules/timer'; -import { useToastContext } from '../../hooks/useToastContext'; +import { useToastContext } from '../../hooks'; import { TextSize, TextType } from '../../models'; import { raceTimeout } from '../../services/handlers/race-timeout'; import { @@ -29,6 +28,7 @@ import { import { savedData } from '../../utils/save-player-data'; import { RaceText } from './templates/race-text/RaceText.template'; +import { RaceTimer } from './templates/race-timer/RaceTimer.template'; import { RaceTrack } from './templates/race-view/RaceTrack.template'; export function Race(): ReactElement { @@ -40,7 +40,6 @@ export function Race(): ReactElement { const [raceId, setRaceId] = useState(null); const [raceReadyTime, setRaceReadyTime] = useState(RACE_READY_COUNTDOWN); - const [raceTime, setRaceTime] = useState(0); const selfPlayerId = useRef(savedData.savedPlayerId); const [isTypeLocked, setIsTypeLocked] = useState(true); const [isSpectator, setIsSpectator] = useState(false); @@ -151,6 +150,7 @@ export function Race(): ReactElement { ) as unknown as string, icon: , + toastHideDelay: 3000, }); } } @@ -163,8 +163,6 @@ export function Race(): ReactElement { useEffect((): void => { if (raceId && raceReadyTime <= 0 && !isSpectator) { - const raceTime = game.racesModel[raceId]?.timeoutDuration; - setRaceTime(raceTime); sendInitialTypeLog(raceId); setIsTypeLocked(false); } @@ -217,10 +215,14 @@ export function Race(): ReactElement {
- +
diff --git a/apps/client/src/pages/race/templates/race-timer/RaceTimer.template.tsx b/apps/client/src/pages/race/templates/race-timer/RaceTimer.template.tsx new file mode 100644 index 00000000..babc8db9 --- /dev/null +++ b/apps/client/src/pages/race/templates/race-timer/RaceTimer.template.tsx @@ -0,0 +1,83 @@ +import { ReactElement, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppPlayerId, AppPlayerLogId, AppRaceId } from '@razor/models'; +import { RootState, store } from '@razor/store'; +import { Timer } from 'apps/client/src/components/molecules/timer/Timer.component'; +import { RECENT_WPM_UPDATE_INTERVAL } from 'apps/client/src/constants/race'; +import { savedData } from 'apps/client/src/utils/save-player-data'; + +import { computeRecentWPM } from './utils/compute-recent-wpm'; + +export interface RaceTimerProps { + raceId: AppRaceId; + isRaceStarted: boolean; + isSpectator?: boolean; + onTimeEnd?: () => void; +} + +export function RaceTimer({ + raceId, + isRaceStarted, + isSpectator = false, + // eslint-disable-next-line @typescript-eslint/no-empty-function + onTimeEnd = (): void => {}, +}: RaceTimerProps): ReactElement { + const recentWpmUpdateInterval = useRef(null); + const [remainingTime, setRemainingTime] = useState(0); + const [recentWPM, setRecentWPM] = useState(0); + const [speedometerPercentage, setSpeedometerPercentage] = useState(0); + const game = useSelector((store: RootState) => store.game); + const selfPlayerId = useRef(savedData.savedPlayerId); + + useEffect((): void => { + if (!isRaceStarted) { + return; + } + const race = game.racesModel[raceId]; + const raceStartedTimestamp = race.startedTimestamp; + const raceTime = race.timeoutDuration; + const time = raceTime - (Date.now() - raceStartedTimestamp) / 1000; // To avoid time drifts + setRemainingTime(time); + }, [raceId, isRaceStarted]); + + useEffect(() => { + if (isSpectator || !isRaceStarted) { + return; + } + + if (recentWpmUpdateInterval.current) { + clearInterval(recentWpmUpdateInterval.current); + } + + recentWpmUpdateInterval.current = setInterval(() => { + const game = store.getState().game; + if (selfPlayerId?.current) { + const playerLogId: AppPlayerLogId = `${raceId}-${selfPlayerId.current}`; + const playerLogs = game.playerLogsModel[playerLogId] || []; + + const wpm = +computeRecentWPM(playerLogs).toFixed(2); + setRecentWPM(wpm); + + // Get percentage from wpm (clamp between 0 and 1) + const percentage = Math.min(Math.max(wpm / 100, 0), 1); + setSpeedometerPercentage(percentage); + } + }, RECENT_WPM_UPDATE_INTERVAL); + + return () => { + if (recentWpmUpdateInterval.current) { + clearInterval(recentWpmUpdateInterval.current); + } + }; + }, [raceId, isRaceStarted, isSpectator]); + + return ( + + ); +} From b26a903fceca185f687cebe43c871f20e986d798 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 21:17:10 +0530 Subject: [PATCH 08/21] feat(race/timer): change color of race-timer when time is low --- .../src/components/molecules/timer/Timer.component.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/client/src/components/molecules/timer/Timer.component.tsx b/apps/client/src/components/molecules/timer/Timer.component.tsx index 1163bdbc..e8af00b9 100644 --- a/apps/client/src/components/molecules/timer/Timer.component.tsx +++ b/apps/client/src/components/molecules/timer/Timer.component.tsx @@ -35,6 +35,9 @@ export function Timer({ const circleIndicator = useRef(null); const timerEnded = useRef(false); + // Consider time is low if time is less than 20% of the total time + const isTimeLow = time > 0 && seconds <= time * 0.15; + useEffect(() => { startTimestamp.current = Date.now(); previousTimestamp.current = 0; @@ -188,7 +191,10 @@ export function Timer({ strokeWidth='15' strokeLinecap='round' transform='rotate(-90,150,150)' - className='stroke-neutral-90' + className={cs( + isTimeLow ? 'stroke-error-60' : 'stroke-neutral-90', + 'transition-all duration-300', + )} /> From 402859e7c228146ebc566eb413a615f3716462af Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 21:20:50 +0530 Subject: [PATCH 09/21] feat(client): minor updates --- .../client/src/services/handlers/join-room.ts | 23 +++++++++++++------ apps/client/src/utils/guardedRoute.tsx | 10 ++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/client/src/services/handlers/join-room.ts b/apps/client/src/services/handlers/join-room.ts index 6e3cfc5a..25dfa22d 100644 --- a/apps/client/src/services/handlers/join-room.ts +++ b/apps/client/src/services/handlers/join-room.ts @@ -2,6 +2,7 @@ import { InitialClientData, InitialServerData, JoinLobbyFailures, + PlayerJoinRejectData, SocketProtocols, } from '@razor/models'; @@ -17,12 +18,16 @@ export const requestToJoinRoom = ({ roomId, }: InitialClientData): Promise => { initializeSocket(); + // TODO: emit sockets from a function like in server socket.emit(SocketProtocols.JoinLobbyRequest, { playerName, roomId }); + console.log(SocketProtocols.JoinLobbyRequest, { playerName, roomId }); const promise: Promise = new Promise((resolve, reject) => { - const receiver = (data: InitialServerData): void => { + const acceptListener = (data: InitialServerData): void => { // remove `T:` part from the tournament id. const roomIdFromServer = data.tournamentId.slice(2); + clearTimeout(waitingTimeout); + socket.off(SocketProtocols.JoinLobbyReject, acceptListener); if (roomIdFromServer) { // For socket communication uses. @@ -32,18 +37,17 @@ export const requestToJoinRoom = ({ data.snapshot.playersModel[data.playerId].name; pubsub.publish(SocketProtocols.JoinLobbyAccept, data); - clearTimeout(waitingTimeout); resolve(roomIdFromServer); } else { reject('Request failed'); } }; - socket.once(SocketProtocols.JoinLobbyAccept, receiver); + socket.once(SocketProtocols.JoinLobbyAccept, acceptListener); - // TODO: Use pubsub instead - socket.once(SocketProtocols.JoinLobbyReject, data => { + const rejectListener = (data: PlayerJoinRejectData): void => { const { message } = data; clearTimeout(waitingTimeout); + socket.off(SocketProtocols.JoinLobbyAccept, acceptListener); let toastMessage = '', toastTitle = ''; @@ -72,10 +76,15 @@ export const requestToJoinRoom = ({ }); reject(data.message); - }); + }; + + // TODO: Use pubsub instead + socket.once(SocketProtocols.JoinLobbyReject, rejectListener); const waitingTimeout = setTimeout(() => { - socket.off(SocketProtocols.JoinLobbyAccept, receiver); + socket.off(SocketProtocols.JoinLobbyAccept, acceptListener); + socket.off(SocketProtocols.JoinLobbyReject, rejectListener); + addToast({ title: 'Timeout', message: 'Request timed out. Server is unreachable.', diff --git a/apps/client/src/utils/guardedRoute.tsx b/apps/client/src/utils/guardedRoute.tsx index cd0178d8..7b2c8e77 100644 --- a/apps/client/src/utils/guardedRoute.tsx +++ b/apps/client/src/utils/guardedRoute.tsx @@ -17,12 +17,14 @@ export function GuardedRoute({ ); useEffect(() => { - const updatePlayerId = savedData.addEventListener(() => { - setPlayerId(savedData.savedPlayerId); - }); + const updatePlayerId = (): void => + savedData.addEventListener(() => { + setPlayerId(savedData.savedPlayerId); + }); + updatePlayerId(); return () => { - savedData.removeEventListener(() => updatePlayerId); + savedData.removeEventListener(updatePlayerId); }; }, []); From 6d8e36dd79b0a82d68a95b97d39a03dacc438dd7 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 21:38:24 +0530 Subject: [PATCH 10/21] refactor(store/effects): move tournament state validation part to setTournamentState effect --- libs/store/src/lib/effects/player.ts | 63 +++++-------------- libs/store/src/lib/effects/tournament.spec.ts | 4 +- libs/store/src/lib/effects/tournament.ts | 26 ++++++-- libs/store/src/lib/payloads/effects.ts | 5 +- 4 files changed, 43 insertions(+), 55 deletions(-) diff --git a/libs/store/src/lib/effects/player.ts b/libs/store/src/lib/effects/player.ts index 31f2011a..3bf9bd29 100644 --- a/libs/store/src/lib/effects/player.ts +++ b/libs/store/src/lib/effects/player.ts @@ -81,31 +81,11 @@ export const joinPlayer = ( // If the tournament is found, set the tournament id. tournamentId = receivedTournamentId; - // Converting tournament state to "Lobby" from "Empty" if it had no players. - if ( - state.game.tournamentsModel[receivedTournamentId].playerIds.length == 0 - ) { - dispatch.game.setTournamentState({ - tournamentId, - tournamentState: AppTournamentState.Lobby, - }); - } - - // Converting tournament state to "Ready" from "Lobby" if it has 2 or more players. - if ( - state.game.tournamentsModel[receivedTournamentId].playerIds.length >= 1 - ) { - const tournamentState = - state.game.tournamentsModel[receivedTournamentId].state; - // Change to Ready only if the tournament is in Lobby state - // (Don't change state if current state is Race or Leaderboard) - if (tournamentState === AppTournamentState.Lobby) { - dispatch.game.setTournamentState({ - tournamentId, - tournamentState: AppTournamentState.Ready, - }); - } - } + // Tournament state validation will handle inside the setTournamentState effect. + dispatch.game.setTournamentState({ + tournamentId, + tournamentState: AppTournamentState.Lobby, + }); } else { // If the tournament id is not provided, generate a new tournament id. tournamentId = generateUid(AppIdNumberType.Tournament); @@ -194,18 +174,11 @@ export const addPlayer = ( return; } - // When adding a player lobby cannot be Empty - // Converting tournament state to "Ready" only if state was "Lobby". - const tournamentState = state.game.tournamentsModel[tournamentId].state; - if ( - state.game.tournamentsModel[tournamentId].playerIds.length >= 1 && - tournamentState === AppTournamentState.Lobby - ) { - dispatch.game.setTournamentState({ - tournamentId, - tournamentState: AppTournamentState.Ready, - }); - } + // Tournament state validation will handle inside the setTournamentState effect. + dispatch.game.setTournamentState({ + tournamentId, + tournamentState: AppTournamentState.Lobby, + }); // Add the new player. dispatch.game.addPlayerReducer({ @@ -263,17 +236,11 @@ export const clearPlayer = ( playerId, }); - // If player was the last member of the tournament, change the tournament state to empty - const playerIdsInTournament = - state.game.tournamentsModel[tournamentId].playerIds; - if (playerIdsInTournament.length === 1) { - if (playerIdsInTournament[0] === playerId) { - dispatch.game.setTournamentState({ - tournamentId, - tournamentState: AppTournamentState.Empty, - }); - } - } + // Tournament state validation will handle inside the setTournamentState effect. + dispatch.game.setTournamentState({ + tournamentId, + tournamentState: AppTournamentState.Lobby, + }); }; /** Effect function for sending player logs while racing. diff --git a/libs/store/src/lib/effects/tournament.spec.ts b/libs/store/src/lib/effects/tournament.spec.ts index 9d29c803..99a3a726 100644 --- a/libs/store/src/lib/effects/tournament.spec.ts +++ b/libs/store/src/lib/effects/tournament.spec.ts @@ -39,7 +39,7 @@ describe('[Effects] Tournament', () => { store.dispatch.game.setTournamentState({ tournamentId: M_TOURNAMENT_ID0, - tournamentState: AppTournamentState.Ready, + tournamentState: AppTournamentState.Lobby, }); const storeState = store.getState(); @@ -70,7 +70,7 @@ describe('[Effects] Tournament', () => { store.dispatch.game.setTournamentState({ tournamentId: 'T:notExist', - tournamentState: AppTournamentState.Ready, + tournamentState: AppTournamentState.Lobby, }); expect(tournamentNotFound).toHaveBeenCalled(); diff --git a/libs/store/src/lib/effects/tournament.ts b/libs/store/src/lib/effects/tournament.ts index 4c8bdbd8..156a0883 100644 --- a/libs/store/src/lib/effects/tournament.ts +++ b/libs/store/src/lib/effects/tournament.ts @@ -1,4 +1,5 @@ -import { AppTournament } from '@razor/models'; +import { MIN_ALLOWED_PLAYERS } from '@razor/constants'; +import { AppTournament, AppTournamentState } from '@razor/models'; import { SetTournamentStatePayload } from '../payloads'; import { tournamentNotFound } from '../raisers'; @@ -6,7 +7,11 @@ import { Dispatch, RootState } from '../store'; /** Effect function for setting tournament state. * Run the validation for the received payload. - * If the tournament is found, then change the state of the tournament. + * If the tournament is found, then validate and change the state of the tournament. + * + * - If tournament state is, `Lobby` validate tournament state and update it. + * (No active players => Empty, MIN_ALLOWED_PLAYERS or more => Ready, else => Lobby) + * - If tournament state of `Race` and `Leaderboard` just update it. * * @param dispatch - The dispatch function of the store. * @param payload - The payload of the action. @@ -26,13 +31,26 @@ export const setTournamentState = ( const { tournamentId, tournamentState } = payload; if (!(tournamentId in state.game.tournamentsModel)) { - tournamentNotFound(dispatch, tournamentId, `While setting ready`); + tournamentNotFound(dispatch, tournamentId, `While setting state`); return; } + // Validate and update tournament state. + const playerIdsInTournament = + state.game.tournamentsModel[tournamentId].playerIds; + + let validatedTournamentState: AppTournamentState = tournamentState; + if (tournamentState === AppTournamentState.Lobby) { + if (playerIdsInTournament.length === 0) { + validatedTournamentState = AppTournamentState.Empty; + } else if (playerIdsInTournament.length >= MIN_ALLOWED_PLAYERS) { + validatedTournamentState = AppTournamentState.Ready; + } + } + const tournament: AppTournament = { ...state.game.tournamentsModel[tournamentId], - state: tournamentState, + state: validatedTournamentState, }; dispatch.game.updateTournamentReducer({ tournamentId, diff --git a/libs/store/src/lib/payloads/effects.ts b/libs/store/src/lib/payloads/effects.ts index 337c6fe3..3be762d7 100644 --- a/libs/store/src/lib/payloads/effects.ts +++ b/libs/store/src/lib/payloads/effects.ts @@ -26,7 +26,10 @@ export type ClearPlayerPayload = { export type SetTournamentStatePayload = { tournamentId: AppTournamentId; - tournamentState: AppTournamentState; + tournamentState: Exclude< + AppTournamentState, + AppTournamentState.Empty | AppTournamentState.Ready + >; }; export type StartRacePayload = { From bcf9e844d69216ab7dc1d4e81c6e0d19f9b9e19f Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 22:03:14 +0530 Subject: [PATCH 11/21] refactor(store/effects): rename setTournamentState to updateTournamentState --- libs/store/src/lib/effects/effects.ts | 10 ++++++---- libs/store/src/lib/effects/player.ts | 18 +++++++++--------- libs/store/src/lib/effects/race.ts | 6 +++--- libs/store/src/lib/effects/tournament.spec.ts | 4 ++-- libs/store/src/lib/effects/tournament.ts | 6 +++--- libs/store/src/lib/payloads/effects.ts | 2 +- 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/libs/store/src/lib/effects/effects.ts b/libs/store/src/lib/effects/effects.ts index d58d9c60..45973aba 100644 --- a/libs/store/src/lib/effects/effects.ts +++ b/libs/store/src/lib/effects/effects.ts @@ -8,8 +8,8 @@ import { JoinPlayerPayload, ReplaceFullStatePayload, SendTypeLogPayload, - SetTournamentStatePayload, StartRacePayload, + UpdateTournamentStatePayload, } from '../payloads'; import { Dispatch, RootState } from '../store'; @@ -17,7 +17,7 @@ import { sendLogMessage } from './logger'; import { addPlayer, clearPlayer, joinPlayer, sendTypeLog } from './player'; import { endRace, startRace } from './race'; import { replaceFullState } from './replacers'; -import { setTournamentState } from './tournament'; +import { updateTournamentState } from './tournament'; /** Effects functions of the store * @@ -30,8 +30,10 @@ export const effects = (dispatch: Dispatch) => ({ addPlayer(dispatch, payload, state), clearPlayer: (payload: ClearPlayerPayload, state: RootState) => clearPlayer(dispatch, payload, state), - setTournamentState: (payload: SetTournamentStatePayload, state: RootState) => - setTournamentState(dispatch, payload, state), + updateTournamentState: ( + payload: UpdateTournamentStatePayload, + state: RootState, + ) => updateTournamentState(dispatch, payload, state), startRace: (payload: StartRacePayload, state: RootState) => startRace(dispatch, payload, state), endRace: (payload: EndRacePayload, state: RootState) => diff --git a/libs/store/src/lib/effects/player.ts b/libs/store/src/lib/effects/player.ts index 3bf9bd29..a7ca4f27 100644 --- a/libs/store/src/lib/effects/player.ts +++ b/libs/store/src/lib/effects/player.ts @@ -36,7 +36,7 @@ import { Dispatch, RootState } from '../store'; * @param state - Current state model. * * ### Related reducers and effects - * - setTournamentState (effect) + * - updateTournamentState (effect) * - addTournamentReducer * - addPlayerReducer * @@ -81,8 +81,8 @@ export const joinPlayer = ( // If the tournament is found, set the tournament id. tournamentId = receivedTournamentId; - // Tournament state validation will handle inside the setTournamentState effect. - dispatch.game.setTournamentState({ + // Tournament state validation will handle inside the updateTournamentState effect. + dispatch.game.updateTournamentState({ tournamentId, tournamentState: AppTournamentState.Lobby, }); @@ -129,7 +129,7 @@ export const joinPlayer = ( * @param state - Current state model. * * ### Related reducers and effects - * - setTournamentState (effect) + * - updateTournamentState (effect) * - addPlayerReducer * * ### Related raisers @@ -174,8 +174,8 @@ export const addPlayer = ( return; } - // Tournament state validation will handle inside the setTournamentState effect. - dispatch.game.setTournamentState({ + // Tournament state validation will handle inside the updateTournamentState effect. + dispatch.game.updateTournamentState({ tournamentId, tournamentState: AppTournamentState.Lobby, }); @@ -202,7 +202,7 @@ export const addPlayer = ( * @param state - Current state model. * * ### Related reducers and effects - * - setTournamentState (effect) + * - updateTournamentState (effect) * - removePlayerReducer * * ### Related raisers @@ -236,8 +236,8 @@ export const clearPlayer = ( playerId, }); - // Tournament state validation will handle inside the setTournamentState effect. - dispatch.game.setTournamentState({ + // Tournament state validation will handle inside the updateTournamentState effect. + dispatch.game.updateTournamentState({ tournamentId, tournamentState: AppTournamentState.Lobby, }); diff --git a/libs/store/src/lib/effects/race.ts b/libs/store/src/lib/effects/race.ts index 122c2456..7959849d 100644 --- a/libs/store/src/lib/effects/race.ts +++ b/libs/store/src/lib/effects/race.ts @@ -32,7 +32,7 @@ import { Dispatch, RootState } from '../store'; * @param state - Current state model. * * ### Related reducers and effects - * - setTournamentState (effect) + * - updateTournamentState (effect) * - updatePlayerReducer * - updateRaceReducer * @@ -121,7 +121,7 @@ export const startRace = ( }; // Updating tournament state in tournamentsModel. ("Ready" -> "Race") - dispatch.game.setTournamentState({ + dispatch.game.updateTournamentState({ tournamentId, tournamentState: AppTournamentState.Race, }); @@ -173,7 +173,7 @@ export const endRace = ( ) as AppTournamentId; // Set tournament state to Leaderboard. - dispatch.game.setTournamentState({ + dispatch.game.updateTournamentState({ tournamentId, tournamentState: AppTournamentState.Leaderboard, }); diff --git a/libs/store/src/lib/effects/tournament.spec.ts b/libs/store/src/lib/effects/tournament.spec.ts index 99a3a726..6d48e197 100644 --- a/libs/store/src/lib/effects/tournament.spec.ts +++ b/libs/store/src/lib/effects/tournament.spec.ts @@ -37,7 +37,7 @@ describe('[Effects] Tournament', () => { const store = initializeStore(initialValues); const initialStoreState = store.getState(); - store.dispatch.game.setTournamentState({ + store.dispatch.game.updateTournamentState({ tournamentId: M_TOURNAMENT_ID0, tournamentState: AppTournamentState.Lobby, }); @@ -68,7 +68,7 @@ describe('[Effects] Tournament', () => { }; const store = initializeStore(initialValues); - store.dispatch.game.setTournamentState({ + store.dispatch.game.updateTournamentState({ tournamentId: 'T:notExist', tournamentState: AppTournamentState.Lobby, }); diff --git a/libs/store/src/lib/effects/tournament.ts b/libs/store/src/lib/effects/tournament.ts index 156a0883..0150ee3b 100644 --- a/libs/store/src/lib/effects/tournament.ts +++ b/libs/store/src/lib/effects/tournament.ts @@ -1,7 +1,7 @@ import { MIN_ALLOWED_PLAYERS } from '@razor/constants'; import { AppTournament, AppTournamentState } from '@razor/models'; -import { SetTournamentStatePayload } from '../payloads'; +import { UpdateTournamentStatePayload } from '../payloads'; import { tournamentNotFound } from '../raisers'; import { Dispatch, RootState } from '../store'; @@ -23,9 +23,9 @@ import { Dispatch, RootState } from '../store'; * ### Related raisers * - tournamentNotFound */ -export const setTournamentState = ( +export const updateTournamentState = ( dispatch: Dispatch, - payload: SetTournamentStatePayload, + payload: UpdateTournamentStatePayload, state: RootState, ): void => { const { tournamentId, tournamentState } = payload; diff --git a/libs/store/src/lib/payloads/effects.ts b/libs/store/src/lib/payloads/effects.ts index 3be762d7..34d0b410 100644 --- a/libs/store/src/lib/payloads/effects.ts +++ b/libs/store/src/lib/payloads/effects.ts @@ -24,7 +24,7 @@ export type ClearPlayerPayload = { playerId: AppPlayerId; }; -export type SetTournamentStatePayload = { +export type UpdateTournamentStatePayload = { tournamentId: AppTournamentId; tournamentState: Exclude< AppTournamentState, From 10465bcff45e772b6f56e4150db24b7c2c3e9a7c Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 22:07:09 +0530 Subject: [PATCH 12/21] fix(store): fix typo in store errors --- libs/models/src/lib/state/log-message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/models/src/lib/state/log-message.ts b/libs/models/src/lib/state/log-message.ts index 2de7adcd..e016191f 100644 --- a/libs/models/src/lib/state/log-message.ts +++ b/libs/models/src/lib/state/log-message.ts @@ -1,6 +1,6 @@ /** Error codes */ export enum AppErrorCode { - TournamentNotExists = 'TOURNAMNET_NOT_FOUND', + TournamentNotExists = 'TOURNAMENT_NOT_FOUND', PlayerNotExists = 'PLAYER_NOT_FOUND', InvalidPlayerName = 'INVALID_PLAYER_NAME', RaceNotExists = 'RACE_NOT_FOUND', From 01520cc678d6910acff9e8213b0c376f13f96946 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 22:36:12 +0530 Subject: [PATCH 13/21] test(store/effects): update tests and fix issues --- libs/store/src/lib/effects/player.spec.ts | 7 ++++++- libs/store/src/lib/effects/player.ts | 24 +++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/libs/store/src/lib/effects/player.spec.ts b/libs/store/src/lib/effects/player.spec.ts index ddbfb149..eca91bc8 100644 --- a/libs/store/src/lib/effects/player.spec.ts +++ b/libs/store/src/lib/effects/player.spec.ts @@ -336,7 +336,12 @@ describe('[Effects] Player', () => { const expectedResult: AppStateModel = { ...initialValues, tournamentsModel: { - [M_TOURNAMENT_ID0]: mockTournament(M_TOURNAMENT_ID0, [0, 1], [1, 3]), + [M_TOURNAMENT_ID0]: mockTournament( + M_TOURNAMENT_ID0, + [0, 1], + [1, 3], + AppTournamentState.Ready, + ), }, playersModel: mockPlayersModel([1, 3], M_TOURNAMENT_ID0), }; diff --git a/libs/store/src/lib/effects/player.ts b/libs/store/src/lib/effects/player.ts index a7ca4f27..ef282e6b 100644 --- a/libs/store/src/lib/effects/player.ts +++ b/libs/store/src/lib/effects/player.ts @@ -80,12 +80,6 @@ export const joinPlayer = ( } // If the tournament is found, set the tournament id. tournamentId = receivedTournamentId; - - // Tournament state validation will handle inside the updateTournamentState effect. - dispatch.game.updateTournamentState({ - tournamentId, - tournamentState: AppTournamentState.Lobby, - }); } else { // If the tournament id is not provided, generate a new tournament id. tournamentId = generateUid(AppIdNumberType.Tournament); @@ -116,6 +110,12 @@ export const joinPlayer = ( }, }); + // Tournament state validation will handle inside the updateTournamentState effect. + dispatch.game.updateTournamentState({ + tournamentId, + tournamentState: AppTournamentState.Lobby, + }); + return playerId; }; @@ -174,12 +174,6 @@ export const addPlayer = ( return; } - // Tournament state validation will handle inside the updateTournamentState effect. - dispatch.game.updateTournamentState({ - tournamentId, - tournamentState: AppTournamentState.Lobby, - }); - // Add the new player. dispatch.game.addPlayerReducer({ tournamentId, @@ -191,6 +185,12 @@ export const addPlayer = ( tournamentId, }, }); + + // Tournament state validation will handle inside the updateTournamentState effect. + dispatch.game.updateTournamentState({ + tournamentId, + tournamentState: AppTournamentState.Lobby, + }); }; /** Effect function for clearing player. From 0b99943757163d4fc91d3b45eb83b6d96fe839cc Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 4 Dec 2023 23:24:11 +0530 Subject: [PATCH 14/21] docs(components/timer): add detailed command on optimizing animation --- .../client/src/components/molecules/timer/Timer.component.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/client/src/components/molecules/timer/Timer.component.tsx b/apps/client/src/components/molecules/timer/Timer.component.tsx index e8af00b9..050de96f 100644 --- a/apps/client/src/components/molecules/timer/Timer.component.tsx +++ b/apps/client/src/components/molecules/timer/Timer.component.tsx @@ -67,6 +67,10 @@ export function Timer({ const now = Date.now(); // Optimizing for high refresh rate screens + // This animation will run on every frame. + // But in high refresh rate screens, time difference between frames is very low. + // Therefore, we are skipping the animation for a specific frame + // where time difference between last and current frames is less than 10ms if (now - previousTimestamp.current < 10) { return; } From a2f934c4d69cdfed5c031f3d9174c8e4a76330e6 Mon Sep 17 00:00:00 2001 From: supunTE Date: Tue, 5 Dec 2023 09:59:25 +0530 Subject: [PATCH 15/21] fix(pages/race): fix variable name typos --- .../race/templates/race-text/RaceText.template.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx b/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx index 29790f5c..c070acc3 100644 --- a/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx +++ b/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx @@ -302,7 +302,7 @@ export function RaceText({ charIndex, { cursorAt: playerCursorAt }, ); - const isCursorAtInvalidCursor = indexConverter.isCursorAtChar( + const isInvalidCursorAtLetter = indexConverter.isCursorAtChar( charIndex, { cursorAt: invalidCursorAt }, ); @@ -322,13 +322,13 @@ export function RaceText({ cursorsAt: otherPlayerCursors, }); /* Show normal cursor if no invalid chars */ - const isVisibleRegularCursor = + const isRegularCursorVisible = (noOfInvalidChars === 0 || isLocked) && isCursorAtLetter; /* Show invalid cursor if invalid chars */ - const isVisibleInvalidCursor = + const isInvalidCursorVisible = noOfInvalidChars > 0 && !isLocked && - isCursorAtInvalidCursor; + isInvalidCursorAtLetter; return ( - {isVisibleRegularCursor ? ( + {isRegularCursorVisible ? ( ) : null} - {isVisibleInvalidCursor ? ( + {isInvalidCursorVisible ? ( ) : null} {isOtherPlayerCursorsOnLetter ? ( From ab14c6e1287fc13ba3a92c9facc56607020b84ff Mon Sep 17 00:00:00 2001 From: supunTE Date: Tue, 5 Dec 2023 10:02:38 +0530 Subject: [PATCH 16/21] refactor: rename Router to AppRouter --- apps/client/src/app.tsx | 4 ++-- apps/client/src/{router.tsx => appRouter.tsx} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/client/src/{router.tsx => appRouter.tsx} (96%) diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index b702e02a..2858362b 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -4,8 +4,8 @@ import { clearGlobalToastManager, setGlobalToastManager, } from './utils/globalToastManager'; +import { AppRouter } from './appRouter'; import { useToastContext } from './hooks'; -import { Router } from './router'; export function App(): ReactElement { const addToast = useToastContext(); @@ -18,5 +18,5 @@ export function App(): ReactElement { }; }, [addToast]); - return ; + return ; } diff --git a/apps/client/src/router.tsx b/apps/client/src/appRouter.tsx similarity index 96% rename from apps/client/src/router.tsx rename to apps/client/src/appRouter.tsx index a7c9777b..e9b678db 100644 --- a/apps/client/src/router.tsx +++ b/apps/client/src/appRouter.tsx @@ -5,7 +5,7 @@ import { NotFound } from './pages/NotFound'; import { GuardedRoute } from './utils/guardedRoute'; import { Home, Layout, Leaderboard, Race, Room } from './pages'; -export function Router(): ReactElement { +export function AppRouter(): ReactElement { return ( From 5feab21e8a824365acd9e9de9cfe2cd16090c19e Mon Sep 17 00:00:00 2001 From: supunTE Date: Tue, 5 Dec 2023 10:08:50 +0530 Subject: [PATCH 17/21] refactor(pages/race): rename raceEndHandler to raceEndHandler --- apps/client/src/pages/race/Race.page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/pages/race/Race.page.tsx b/apps/client/src/pages/race/Race.page.tsx index 45097d20..b1262ef0 100644 --- a/apps/client/src/pages/race/Race.page.tsx +++ b/apps/client/src/pages/race/Race.page.tsx @@ -168,7 +168,7 @@ export function Race(): ReactElement { } }, [raceReadyTime, raceId]); - const raceTimeEndHandler = (): void => { + const raceEndHandler = (): void => { setIsTypeLocked(true); if (raceId) { raceTimeout(raceId); @@ -221,7 +221,7 @@ export function Race(): ReactElement {
From 185c85be88d2c38087ab68b5ad77bcb1a377ec4c Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 18 Mar 2024 21:40:38 +0530 Subject: [PATCH 18/21] refactor: update content --- apps/client/src/i18n/en/race.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/i18n/en/race.json b/apps/client/src/i18n/en/race.json index eb77756d..ba7fffa2 100644 --- a/apps/client/src/i18n/en/race.json +++ b/apps/client/src/i18n/en/race.json @@ -7,7 +7,7 @@ }, "reconnected_as_new": { "title": "Reconnected as a New Player", - "message": "Unfortunately, you were removed from the race after an extended disconnection. Despite your reconnection, the server had to remove you. You'll now be treated as a new player in the tournament." + "message": "Unfortunately, you were removed from the race after an extended period of inactivity. Despite your reconnection, the server had to remove you. You'll now be treated as a new player in the tournament." } } } From 782e189af874a6e3126189a31dea5259a4d6fe48 Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 18 Mar 2024 21:54:52 +0530 Subject: [PATCH 19/21] refactor: update content --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fa99c91..f16a49eb 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start:server": "nx serve server", "start:server-network": "nx serve server --host=0.0.0.0", "storybook": "nx run client:storybook", - "build-storybook": "nx run client:build-storybook", + "build-storybook": "nx run client:build-storybook -- --webpack-stats-json", "build": "nx build client && nx build server", "test": "nx run-many --all --target=test && npm run coverage:merge && npm run coverage:summary", "coverage:codecov": "codecov", From d0acb545757166d31d30a431fdec128b02e1d09b Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 18 Mar 2024 22:06:16 +0530 Subject: [PATCH 20/21] ci: update ci script --- .github/workflows/onPR.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/onPR.yml b/.github/workflows/onPR.yml index d820f8af..d81a28e6 100644 --- a/.github/workflows/onPR.yml +++ b/.github/workflows/onPR.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.19.0] + node-version: [ 16.19.0 ] steps: - uses: actions/checkout@v2 with: @@ -26,6 +26,7 @@ jobs: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }} onlyChanged: true + storybookBaseDir: dist/storybook/client autoAcceptChanges: main - if: steps.chromatic.outputs.buildUrl != 'undefined' name: Publish Summary From 179bdf74eac8e221ca71eaaaa42c658889a5164a Mon Sep 17 00:00:00 2001 From: supunTE Date: Mon, 18 Mar 2024 22:09:39 +0530 Subject: [PATCH 21/21] revert: revert build and ci scripts --- .github/workflows/onPR.yml | 2 -- package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/onPR.yml b/.github/workflows/onPR.yml index d81a28e6..41a816c4 100644 --- a/.github/workflows/onPR.yml +++ b/.github/workflows/onPR.yml @@ -25,8 +25,6 @@ jobs: with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }} - onlyChanged: true - storybookBaseDir: dist/storybook/client autoAcceptChanges: main - if: steps.chromatic.outputs.buildUrl != 'undefined' name: Publish Summary diff --git a/package.json b/package.json index f16a49eb..5fa99c91 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start:server": "nx serve server", "start:server-network": "nx serve server --host=0.0.0.0", "storybook": "nx run client:storybook", - "build-storybook": "nx run client:build-storybook -- --webpack-stats-json", + "build-storybook": "nx run client:build-storybook", "build": "nx build client && nx build server", "test": "nx run-many --all --target=test && npm run coverage:merge && npm run coverage:summary", "coverage:codecov": "codecov",