Skip to content

Commit 247ca66

Browse files
committed
add New Badge Modal
1 parent 13d6384 commit 247ca66

File tree

19 files changed

+255
-48
lines changed

19 files changed

+255
-48
lines changed

client/src/components/BadgesScreen/BadgesScreen.jsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
1-
import { useContext } from "react";
1+
import { useContext, useEffect, useState } from "react";
22

33
// components
4-
import { BackButton } from "@components";
4+
import { BackButton, Loading } from "@components";
55

66
// context
77
import { GlobalDispatchContext, GlobalStateContext } from "@context/GlobalContext";
8-
import { SCREEN_MANAGER } from "@context/types";
8+
import { SCREEN_MANAGER, SET_VISITOR_INVENTORY, SET_ERROR } from "@context/types";
9+
10+
// utils
11+
import { backendAPI, getErrorMessage } from "@utils";
912

1013
export const BadgesScreen = () => {
1114
const dispatch = useContext(GlobalDispatchContext);
1215
const { badges, visitorInventory } = useContext(GlobalStateContext);
1316

17+
const [isLoading, setIsLoading] = useState(false);
18+
19+
useEffect(() => {
20+
setIsLoading(true);
21+
22+
const getVisitorInventory = async () => {
23+
await backendAPI
24+
.get("/visitor-inventory")
25+
.then((response) => {
26+
dispatch({ type: SET_VISITOR_INVENTORY, payload: response.data });
27+
})
28+
.catch((error) => {
29+
dispatch({
30+
type: SET_ERROR,
31+
payload: { error: getErrorMessage("getting visitor inventory", error) },
32+
});
33+
})
34+
.finally(() => {
35+
setIsLoading(false);
36+
});
37+
};
38+
getVisitorInventory();
39+
}, []);
40+
41+
if (isLoading) return <Loading />;
42+
1443
return (
1544
<>
1645
<BackButton onClick={() => dispatch({ type: SCREEN_MANAGER.SHOW_HOME_SCREEN })} />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import PropTypes from "prop-types";
2+
3+
// context
4+
import { GlobalDispatchContext } from "@context/GlobalContext";
5+
import { SCREEN_MANAGER } from "@context/types";
6+
import { useContext } from "react";
7+
8+
export const NewBadgeModal = ({ badge, handleToggleShowModal }) => {
9+
const dispatch = useContext(GlobalDispatchContext);
10+
const { name, icon } = badge;
11+
12+
return (
13+
<div className="modal-container">
14+
<div className="modal">
15+
<div className="modal-header flex gap-2 grid-cols-2">
16+
<h3 className="flex-grow text-left">New Badge Unlocked!</h3>
17+
<button onClick={handleToggleShowModal}>
18+
<img src="https://sdk-style.s3.amazonaws.com/icons/x.svg" style={{ width: "10px" }} />
19+
</button>
20+
</div>
21+
<div className="grid gap-6 text-center pt-2">
22+
<img src={icon} alt={name} style={{ width: "100px", height: "100px", margin: "auto" }} />
23+
<p>
24+
<strong>{name}</strong>
25+
</p>
26+
<button className="btn-secondary" onClick={() => dispatch({ type: SCREEN_MANAGER.SHOW_BADGES_SCREEN })}>
27+
View all Badges
28+
</button>
29+
</div>
30+
</div>
31+
</div>
32+
);
33+
};
34+
35+
NewBadgeModal.propTypes = {
36+
badge: PropTypes.shape({
37+
name: PropTypes.string,
38+
icon: PropTypes.string,
39+
}),
40+
handleToggleShowModal: PropTypes.func,
41+
};
42+
43+
export default NewBadgeModal;

client/src/components/RaceCompletedScreen/RaceCompletedScreen.jsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
1-
import { useContext } from "react";
1+
import { useContext, useEffect, useState } from "react";
2+
import { useSearchParams } from "react-router-dom";
23

34
// components
4-
import { Footer, Tabs } from "@components";
5+
import { Footer, NewBadgeModal, Tabs } from "@components";
56

67
// context
78
import { GlobalStateContext, GlobalDispatchContext } from "@context/GlobalContext";
89
import { SCREEN_MANAGER } from "@context/types";
910

1011
export const RaceCompletedScreen = () => {
1112
const dispatch = useContext(GlobalDispatchContext);
12-
const { elapsedTime } = useContext(GlobalStateContext);
13+
const { elapsedTime, badges } = useContext(GlobalStateContext);
1314

14-
function handlePlayAgain() {
15-
dispatch({ type: SCREEN_MANAGER.SHOW_HOME_SCREEN });
16-
}
15+
const [newBadgeKey, setNewBadgeKey] = useState();
16+
17+
const [searchParams] = useSearchParams();
18+
const profileId = searchParams.get("profileId");
19+
20+
useEffect(() => {
21+
if (profileId) {
22+
const eventSource = new EventSource(`/api/events?profileId=${profileId}`);
23+
eventSource.onmessage = function (event) {
24+
const newEvent = JSON.parse(event.data);
25+
if (newEvent.badgeKey) setNewBadgeKey(newEvent.badgeKey);
26+
};
27+
eventSource.onerror = (event) => {
28+
console.error("Server Event error:", event);
29+
};
30+
return () => {
31+
eventSource.close();
32+
};
33+
}
34+
}, [profileId]);
1735

1836
return (
1937
<>
@@ -35,10 +53,12 @@ export const RaceCompletedScreen = () => {
3553
</div>
3654

3755
<Footer>
38-
<button className="btn-primary" onClick={() => handlePlayAgain()}>
56+
<button className="btn-primary" onClick={() => dispatch({ type: SCREEN_MANAGER.SHOW_HOME_SCREEN })}>
3957
Play Again
4058
</button>
4159
</Footer>
60+
61+
{newBadgeKey && <NewBadgeModal badge={badges[newBadgeKey]} handleToggleShowModal={() => {}} />}
4262
</>
4363
);
4464
};

client/src/components/RaceInProgressScreen/RaceInProgressScreen.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export const RaceInProgressScreen = () => {
123123
if (allCompleted && !completeRaceCalledRef.current) {
124124
completeRaceCalledRef.current = true;
125125
successAudioRef.current.play();
126-
completeRace({ dispatch, currentFinishedElapsedTime });
126+
completeRace({ dispatch });
127127
}
128128
}, [checkpoints, isFinishComplete, currentFinishedElapsedTime, dispatch]);
129129

client/src/components/SwitchRace/SwitchTrackScreen.jsx

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,57 @@
1-
import { useState, useContext } from "react";
1+
import { useState, useContext, useEffect } from "react";
22

33
// components
44
import { BackButton, Footer } from "@components";
55

66
// context
77
import { GlobalDispatchContext, GlobalStateContext } from "@context/GlobalContext";
8-
import { SET_ERROR, SCREEN_MANAGER } from "@context/types";
8+
import { SET_ERROR, SCREEN_MANAGER, SET_SCENE_DATA } from "@context/types";
99

1010
// utils
1111
import { backendAPI, getErrorMessage } from "@utils";
1212

1313
export const SwitchTrackScreen = () => {
1414
const dispatch = useContext(GlobalDispatchContext);
15-
const { tracks } = useContext(GlobalStateContext);
15+
const { tracks, trackLastSwitchedDate } = useContext(GlobalStateContext);
1616

1717
const [selectedTrack, setSelectedTrack] = useState(null);
18-
const [areAllButtonsDisabled, setAreAllButtonsDisabled] = useState(false);
18+
const [areAllButtonsDisabled, setAreAllButtonsDisabled] = useState(true);
19+
20+
useEffect(() => {
21+
if (trackLastSwitchedDate) {
22+
const lastSwitch = trackLastSwitchedDate;
23+
const now = new Date().getTime();
24+
const diffMs = now - lastSwitch;
25+
const diffMinutes = diffMs / (100 * 60);
26+
setAreAllButtonsDisabled(diffMinutes < 30);
27+
} else {
28+
setAreAllButtonsDisabled(false);
29+
}
30+
}, [trackLastSwitchedDate]);
1931

2032
const updateTrack = async () => {
2133
setAreAllButtonsDisabled(true);
2234

2335
await backendAPI
2436
.post(`/race/switch-track?trackSceneId=${selectedTrack.sceneId}`)
37+
.then((response) => {
38+
const { leaderboard, numberOfCheckpoints, trackLastSwitchedDate } = response.data.sceneData;
39+
40+
dispatch({
41+
type: SET_SCENE_DATA,
42+
payload: {
43+
leaderboard,
44+
numberOfCheckpoints,
45+
tracks,
46+
trackLastSwitchedDate,
47+
},
48+
});
49+
})
2550
.catch((error) => {
2651
dispatch({
2752
type: SET_ERROR,
2853
payload: { error: getErrorMessage("resetting", error) },
2954
});
30-
})
31-
.finally(() => {
3255
setAreAllButtonsDisabled(false);
3356
});
3457
};
@@ -43,18 +66,20 @@ export const SwitchTrackScreen = () => {
4366
</div>
4467

4568
<div className="grid grid-cols-2 gap-6">
46-
{tracks?.map((track) => (
47-
<button
48-
key={track.id}
49-
className={`mb-2 ${selectedTrack === track.id ? "selected" : ""}`}
50-
onClick={() => setSelectedTrack(track)}
51-
>
52-
<div className="tooltip">
53-
<span className="tooltip-content">{track.name}</span>
54-
<img className="track object-cover" src={track?.thumbnail} alt={track.name} />
55-
</div>
56-
</button>
57-
))}
69+
{tracks?.map((track) => {
70+
return (
71+
<button key={track.id} className="mb-2" onClick={() => setSelectedTrack(track)}>
72+
<div className="tooltip">
73+
<span className="tooltip-content">{track.name}</span>
74+
<img
75+
className={`track object-cover ${selectedTrack && selectedTrack.id === track.id ? "selected" : ""}`}
76+
src={track?.thumbnail}
77+
alt={track.name}
78+
/>
79+
</div>
80+
</button>
81+
);
82+
})}
5883
</div>
5984

6085
<Footer>

client/src/components/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from "./Leaderboard/LeaderboardScreen.jsx";
55
export * from "./BadgesScreen/BadgesScreen.jsx";
66
export * from "./NewGameScreen/NewGameScreen.jsx";
77
export * from "./OnYourMarkScreen/OnYourMarkScreen.jsx";
8+
export * from "./RaceCompletedScreen/NewBadgeModal.jsx";
89
export * from "./RaceCompletedScreen/RaceCompletedScreen.jsx";
910
export * from "./RaceInProgressScreen/Checkpoint.jsx";
1011
export * from "./RaceInProgressScreen/RaceInProgressScreen.jsx";

client/src/context/reducer.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
CANCEL_RACE,
77
LOAD_GAME_STATE,
88
RESET_GAME,
9+
SET_VISITOR_INVENTORY,
10+
SET_SCENE_DATA,
911
SET_ERROR,
1012
} from "./types";
1113

@@ -68,6 +70,7 @@ const globalReducer = (state, action) => {
6870
...state,
6971
screenManager: SCREEN_MANAGER.SHOW_RACE_COMPLETED_SCREEN,
7072
elapsedTime: payload.elapsedTime,
73+
visitorInventory: payload.visitorInventory,
7174
error: "",
7275
};
7376
case CANCEL_RACE:
@@ -97,6 +100,21 @@ const globalReducer = (state, action) => {
97100
tracks: payload.tracks,
98101
visitorInventory: payload.visitorInventory,
99102
badges: payload.badges,
103+
trackLastSwitchedDate: payload.trackLastSwitchedDate,
104+
error: "",
105+
};
106+
case SET_VISITOR_INVENTORY:
107+
return {
108+
...state,
109+
visitorInventory: payload.visitorInventory,
110+
error: "",
111+
};
112+
case SET_SCENE_DATA:
113+
return {
114+
...state,
115+
leaderboard: payload.leaderboard,
116+
numberOfCheckpoints: payload.numberOfCheckpoints,
117+
trackLastSwitchedDate: payload.trackLastSwitchedDate,
100118
error: "",
101119
};
102120
case SET_ERROR:

client/src/context/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export const START_RACE = "START_RACE";
55
export const COMPLETE_RACE = "COMPLETE_RACE";
66
export const CANCEL_RACE = "CANCEL_RACE";
77
export const RESET_GAME = "RESET_GAME";
8+
export const SET_VISITOR_INVENTORY = "SET_VISITOR_INVENTORY";
9+
export const SET_SCENE_DATA = "SET_SCENE_DATA";
810
export const SCREEN_MANAGER = {
911
SHOW_HOME_SCREEN: "SHOW_HOME_SCREEN",
1012
SHOW_LEADERBOARD_SCREEN: "SHOW_LEADERBOARD_SCREEN",

client/src/index.scss

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@
4040
background-color: #ffffff;
4141
}
4242

43+
.btn-primary:disabled {
44+
background-color: #e2dfd7;
45+
border: 4px solid #ffd880;
46+
color: #ffffff;
47+
}
48+
49+
.btn-secondary {
50+
background-color: #1a8dff;
51+
border: 4px solid #1a8dff;
52+
color: #ffffff;
53+
padding: 12px 20px;
54+
border-radius: 50px;
55+
font-size: 18px;
56+
font-weight: bold;
57+
width: 100%;
58+
}
59+
4360
.icon-btn {
4461
cursor: pointer;
4562
color: white;
@@ -75,6 +92,10 @@
7592
height: 120px;
7693
}
7794

95+
.selected {
96+
border: 2px solid #fdb41d;
97+
}
98+
7899
table {
79100
tr:nth-child(even) {
80101
background-color: #f8f8f8;

client/src/utils/completeRace.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { backendAPI, getErrorMessage } from "@utils";
22
import { SET_ERROR, COMPLETE_RACE } from "@context/types";
33

4-
export const completeRace = async ({ dispatch, currentFinishedElapsedTime }) => {
4+
export const completeRace = async ({ dispatch }) => {
55
try {
66
const result = await backendAPI.post("/race/complete-race");
77
if (result.status === 200) {
88
dispatch({
99
type: COMPLETE_RACE,
1010
payload: {
11-
elapsedTime: currentFinishedElapsedTime,
11+
elapsedTime: result.data.elapsedTime,
12+
visitorInventory: result.data.visitorInventory,
1213
},
1314
});
1415
}

0 commit comments

Comments
 (0)