@four-leaf-studios/rl-overlay 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +70 -0
- package/dist/index.cjs.js +271 -10567
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +300 -10579
- package/dist/index.esm.js.map +1 -1
- package/dist/types/Overlay.d.ts +5 -6
- package/dist/types/OverlaySlot.d.ts +9 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/registry.d.ts +2 -0
- package/package.json +12 -5
- package/src/Overlay.tsx +43 -41
- package/src/OverlaySlot.tsx +35 -0
- package/src/Player.tsx +1 -0
- package/src/PlayerBoost.tsx +4 -1
- package/src/Scoreboard.tsx +1 -0
- package/src/ScoreboardGameBox.tsx +2 -4
- package/src/ScoreboardSeriesBox.tsx +2 -3
- package/src/ScoreboardTeam.tsx +1 -0
- package/src/StatItem.tsx +4 -1
- package/src/TargetBoost.tsx +1 -0
- package/src/TargetPlayer.tsx +15 -3
- package/src/TargetPlayerLocation.tsx +1 -0
- package/src/TargetPlayerStats.tsx +1 -1
- package/src/Team.tsx +1 -0
- package/src/Timer.tsx +7 -4
- package/src/css/Florida_Tech.css +696 -0
- package/src/css/Horizon_Hues.css +618 -0
- package/src/css/Neo_Glass.css +674 -0
- package/src/css/Obsidian_Glide.css +646 -0
- package/src/css/RLCS_2024.css +566 -0
- package/src/css/RLCS_2025.css +524 -0
- package/src/css/Shaded_blocks.css +594 -0
- package/src/index.ts +2 -0
- package/src/registry.ts +19 -0
- package/src/types.d.ts +20 -0
- package/test-overlay/public/mock-css.css +294 -372
- package/test-overlay/src/App.jsx +28 -28
- package/tests/BroadcastContext.test.tsx +41 -0
- package/tests/Overlay.test.tsx +106 -0
- package/tests/OverlaySlot.test.tsx +79 -0
- package/tests/PlayerBoost.test.tsx +47 -0
- package/tests/Replay.test.tsx +29 -0
- package/tests/ScoreboardGameBox.test.tsx +35 -0
- package/tests/ScoreboardSeriesBox.test.tsx +48 -0
- package/tests/StatItem.test.tsx +33 -0
- package/tests/__mocks__/@four-leaf-studios/rl-socket-hook.ts +10 -0
- package/tests/fixtures.ts +96 -0
- package/tests/registry.test.ts +27 -0
- package/tests/setup.ts +1 -0
- package/tsconfig.json +16 -20
- package/vitest.config.ts +9 -0
package/dist/types/Overlay.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import type { Broadcast } from "./types";
|
|
3
|
-
import { CSSJSON } from "./hooks/useOverlayStyles";
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Broadcast, OverlayObject } from "./types";
|
|
4
3
|
import "./css/reset.css";
|
|
5
4
|
export type OverlayProps = {
|
|
6
5
|
broadcast: Broadcast;
|
|
7
|
-
|
|
8
|
-
children?: ReactNode;
|
|
6
|
+
overlay: OverlayObject;
|
|
9
7
|
preview?: boolean;
|
|
8
|
+
renderSlot?: (comp: OverlayObject["components"][number], Comp: any) => React.ReactNode;
|
|
10
9
|
};
|
|
11
10
|
export declare const Overlay: {
|
|
12
|
-
({ broadcast,
|
|
11
|
+
({ broadcast, overlay, preview, renderSlot, }: OverlayProps): React.JSX.Element;
|
|
13
12
|
Scoreboard: () => React.JSX.Element | null;
|
|
14
13
|
Teams: () => React.JSX.Element;
|
|
15
14
|
TargetPlayer: React.MemoExoticComponent<() => React.JSX.Element | null>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { OverlayComponentData } from "./types";
|
|
3
|
+
type OverlaySlotProps = {
|
|
4
|
+
component: OverlayComponentData;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
7
|
+
export declare const OverlaySlot: ({ component, children, ...rest }: OverlaySlotProps) => React.JSX.Element;
|
|
8
|
+
declare const _default: React.MemoExoticComponent<({ component, children, ...rest }: OverlaySlotProps) => React.JSX.Element>;
|
|
9
|
+
export default _default;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -13,5 +13,7 @@ export * from "./TargetPlayerLocation";
|
|
|
13
13
|
export * from "./TargetPlayerStats";
|
|
14
14
|
export * from "./Team";
|
|
15
15
|
export * from "./Timer";
|
|
16
|
+
export * from "./OverlaySlot";
|
|
16
17
|
export * from "@four-leaf-studios/rl-socket-hook";
|
|
17
18
|
export * from "./hooks/index";
|
|
19
|
+
export * from "./registry";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@four-leaf-studios/rl-overlay",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "dist/index.cjs.js",
|
|
5
5
|
"module": "dist/index.esm.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "rollup -c",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
12
14
|
"prepare": "npm run build"
|
|
13
15
|
},
|
|
14
16
|
"repository": {
|
|
@@ -23,6 +25,8 @@
|
|
|
23
25
|
},
|
|
24
26
|
"homepage": "https://github.com/Four-Leaf-Studios/rl-overlay#readme",
|
|
25
27
|
"peerDependencies": {
|
|
28
|
+
"@four-leaf-studios/rl-socket-hook": "^1.2.4",
|
|
29
|
+
"framer-motion": "^12.9.4",
|
|
26
30
|
"react": "^18 || ^19",
|
|
27
31
|
"react-dom": "^18 || ^19"
|
|
28
32
|
},
|
|
@@ -33,18 +37,21 @@
|
|
|
33
37
|
"@rollup/plugin-replace": "^6.0.2",
|
|
34
38
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
35
39
|
"@rollup/plugin-url": "^8.0.2",
|
|
40
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
41
|
+
"@testing-library/react": "^16.3.2",
|
|
36
42
|
"@types/postcss-js": "^4.0.4",
|
|
37
43
|
"@types/react": "^19.1.2",
|
|
44
|
+
"@types/react-dom": "^19.2.3",
|
|
45
|
+
"jsdom": "^29.1.1",
|
|
38
46
|
"postcss-url": "^10.1.3",
|
|
39
47
|
"rollup": "^4.40.1",
|
|
40
48
|
"rollup-plugin-copy": "^3.5.0",
|
|
41
49
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
42
50
|
"rollup-plugin-postcss": "^4.0.2",
|
|
43
|
-
"typescript": "^5.8.3"
|
|
51
|
+
"typescript": "^5.8.3",
|
|
52
|
+
"vitest": "^4.1.6"
|
|
44
53
|
},
|
|
45
54
|
"dependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"@rysh/json-to-css": "^1.0.0",
|
|
48
|
-
"framer-motion": "^12.9.4"
|
|
55
|
+
"@rysh/json-to-css": "^1.0.0"
|
|
49
56
|
}
|
|
50
57
|
}
|
package/src/Overlay.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// src/Overlay.tsx
|
|
2
1
|
"use client";
|
|
3
2
|
|
|
4
|
-
import React
|
|
3
|
+
import React from "react";
|
|
5
4
|
import { RLProvider, WebsocketData } from "@four-leaf-studios/rl-socket-hook";
|
|
6
5
|
import { BroadcastProvider } from "./context/BroadcastContext";
|
|
7
|
-
import type { Broadcast } from "./types";
|
|
6
|
+
import type { Broadcast, OverlayObject } from "./types";
|
|
8
7
|
|
|
9
8
|
import Scoreboard from "./Scoreboard";
|
|
10
9
|
import Teams from "./Teams";
|
|
@@ -12,37 +11,38 @@ import TargetPlayer from "./TargetPlayer";
|
|
|
12
11
|
import TargetBoost from "./TargetBoost";
|
|
13
12
|
import Replay from "./Replay";
|
|
14
13
|
|
|
15
|
-
import { useOverlayStyles, CSSJSON } from "./hooks/useOverlayStyles";
|
|
16
14
|
import "./css/reset.css";
|
|
15
|
+
import { componentRegistry } from "./registry";
|
|
16
|
+
import OverlaySlot from "./OverlaySlot";
|
|
17
17
|
|
|
18
18
|
export type OverlayProps = {
|
|
19
19
|
broadcast: Broadcast;
|
|
20
|
-
|
|
21
|
-
children?: ReactNode;
|
|
20
|
+
overlay: OverlayObject;
|
|
22
21
|
preview?: boolean;
|
|
22
|
+
renderSlot?: (
|
|
23
|
+
comp: OverlayObject["components"][number],
|
|
24
|
+
Comp: any
|
|
25
|
+
) => React.ReactNode;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
export const Overlay = ({
|
|
26
29
|
broadcast,
|
|
27
|
-
|
|
30
|
+
overlay,
|
|
28
31
|
preview,
|
|
29
|
-
|
|
32
|
+
renderSlot,
|
|
30
33
|
}: OverlayProps) => {
|
|
31
|
-
|
|
34
|
+
const { components } = overlay;
|
|
35
|
+
|
|
36
|
+
// Collect CSS into one block
|
|
37
|
+
const cssString = components.map((c) => c.css).join("\n");
|
|
32
38
|
|
|
33
39
|
return (
|
|
34
40
|
<BroadcastProvider broadcast={broadcast}>
|
|
35
41
|
<RLProvider>
|
|
36
|
-
<div
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
width: "100%",
|
|
41
|
-
height: "100%",
|
|
42
|
-
maxWidth: "100%",
|
|
43
|
-
maxHeight: "100%",
|
|
44
|
-
}}
|
|
45
|
-
>
|
|
42
|
+
<div className="overlay-wrapper">
|
|
43
|
+
{/* Inject all component CSS */}
|
|
44
|
+
<style>{cssString}</style>
|
|
45
|
+
|
|
46
46
|
{/* The overlay itself */}
|
|
47
47
|
<div
|
|
48
48
|
className={`overlay ${preview ? "testing" : ""}`}
|
|
@@ -56,31 +56,33 @@ export const Overlay = ({
|
|
|
56
56
|
display: "block",
|
|
57
57
|
}}
|
|
58
58
|
>
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
{components.map((comp) => {
|
|
60
|
+
const Comp = componentRegistry[comp.code_id];
|
|
61
|
+
if (!Comp) {
|
|
62
|
+
return (
|
|
63
|
+
<div key={comp.id} style={{ color: "red", fontSize: "14px" }}>
|
|
64
|
+
Missing component: {comp.code_id}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (renderSlot) {
|
|
70
|
+
// delegate to editor if provided
|
|
71
|
+
return renderSlot(comp, Comp);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// default (non-edit mode)
|
|
75
|
+
return (
|
|
76
|
+
<OverlaySlot key={comp.id} component={comp}>
|
|
77
|
+
<Comp {...comp} />
|
|
78
|
+
</OverlaySlot>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
68
81
|
</div>
|
|
69
82
|
|
|
70
83
|
{/* Preview data panel on the right */}
|
|
71
84
|
{preview && (
|
|
72
|
-
<div
|
|
73
|
-
className="testing-data"
|
|
74
|
-
style={{
|
|
75
|
-
flexGrow: 1,
|
|
76
|
-
marginLeft: "1rem",
|
|
77
|
-
overflowY: "auto",
|
|
78
|
-
backgroundColor: "rgba(0,0,0,0.8)",
|
|
79
|
-
color: "#black",
|
|
80
|
-
fontFamily: "monospace",
|
|
81
|
-
maxHeight: "1080px",
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
85
|
+
<div className="testing-data">
|
|
84
86
|
<WebsocketData />
|
|
85
87
|
</div>
|
|
86
88
|
)}
|
|
@@ -90,7 +92,7 @@ export const Overlay = ({
|
|
|
90
92
|
);
|
|
91
93
|
};
|
|
92
94
|
|
|
93
|
-
// expose slots
|
|
95
|
+
// expose slots (optional if you want named exports)
|
|
94
96
|
Overlay.Scoreboard = Scoreboard;
|
|
95
97
|
Overlay.Teams = Teams;
|
|
96
98
|
Overlay.TargetPlayer = TargetPlayer;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { memo, type ReactNode } from "react";
|
|
2
|
+
import type { OverlayComponentData } from "./types";
|
|
3
|
+
|
|
4
|
+
type OverlaySlotProps = {
|
|
5
|
+
component: OverlayComponentData;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
} & React.HTMLAttributes<HTMLDivElement>; // ✅ allow div props
|
|
8
|
+
|
|
9
|
+
export const OverlaySlot = ({
|
|
10
|
+
component,
|
|
11
|
+
children,
|
|
12
|
+
...rest
|
|
13
|
+
}: OverlaySlotProps) => {
|
|
14
|
+
const { id, name, position } = component;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className="overlay-slot"
|
|
19
|
+
data-component-id={id}
|
|
20
|
+
data-component-name={name}
|
|
21
|
+
style={{
|
|
22
|
+
position: "absolute",
|
|
23
|
+
top: position?.top ?? 0,
|
|
24
|
+
left: position?.left ?? 0,
|
|
25
|
+
width: position?.width ?? "auto",
|
|
26
|
+
height: position?.height ?? "auto",
|
|
27
|
+
}}
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default memo(OverlaySlot);
|
package/src/Player.tsx
CHANGED
package/src/PlayerBoost.tsx
CHANGED
|
@@ -12,7 +12,10 @@ export const PlayerBoost = ({ team, boost }: Props) => {
|
|
|
12
12
|
const modifier = team === 0 ? "left" : "right";
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<div
|
|
15
|
+
<div
|
|
16
|
+
data-component-id="PlayerBoost"
|
|
17
|
+
className={`boost_meter ${modifier}_boost_meter`}
|
|
18
|
+
>
|
|
16
19
|
<motion.div
|
|
17
20
|
className={`boost_meter_bar ${modifier}_boost_meter_bar`}
|
|
18
21
|
initial={{ width: "0%" }}
|
package/src/Scoreboard.tsx
CHANGED
|
@@ -24,6 +24,7 @@ export const Scoreboard = () => {
|
|
|
24
24
|
<AnimatePresence mode="wait">
|
|
25
25
|
<motion.div
|
|
26
26
|
key={broadcast.id} // Ensures animation runs when broadcast updates
|
|
27
|
+
data-component-id="Scoreboard"
|
|
27
28
|
className="scoreboard_box"
|
|
28
29
|
initial={{ y: -100, opacity: 0 }}
|
|
29
30
|
animate={{ y: 0, opacity: 1 }}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import { useMemo } from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
4
2
|
import { Broadcast } from "./types";
|
|
5
3
|
|
|
6
4
|
interface ScoreboardGameBoxProps {
|
|
@@ -18,7 +16,7 @@ export const ScoreboardGameBox: React.FC<ScoreboardGameBoxProps> = ({
|
|
|
18
16
|
}, [broadcast.teams]);
|
|
19
17
|
|
|
20
18
|
return (
|
|
21
|
-
<div className="game_box">
|
|
19
|
+
<div data-component-id="ScoreboardGameBox" className="game_box">
|
|
22
20
|
Game {gameNumber} - BO {broadcast.series_number}
|
|
23
21
|
</div>
|
|
24
22
|
);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import { memo } from "react";
|
|
1
|
+
import React, { memo } from "react";
|
|
4
2
|
import { Team } from "./types";
|
|
5
3
|
|
|
6
4
|
interface ScoreboardSeriesBoxProps {
|
|
@@ -25,6 +23,7 @@ export const ScoreboardSeriesBoxComponent: React.FC<
|
|
|
25
23
|
|
|
26
24
|
return (
|
|
27
25
|
<div
|
|
26
|
+
data-component-id="ScoreboardSeriesBox"
|
|
28
27
|
className={`series_box ${modifier}_series_box`}
|
|
29
28
|
style={{ "--team-color": teamColor } as React.CSSProperties}
|
|
30
29
|
>
|
package/src/ScoreboardTeam.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export const ScoreboardTeam: React.FC<ScoreboardTeamProps> = ({ team }) => {
|
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
20
|
<div
|
|
21
|
+
data-component-id="ScoreboardTeam"
|
|
21
22
|
className={`scoreboard_team_box ${modifier}_scoreboard_team_box`}
|
|
22
23
|
style={{ "--team-color": teamColor } as React.CSSProperties}
|
|
23
24
|
>
|
package/src/StatItem.tsx
CHANGED
|
@@ -22,7 +22,10 @@ export const StatItem = <K extends PrimitiveKeys<PlayerState>>({
|
|
|
22
22
|
label,
|
|
23
23
|
value,
|
|
24
24
|
}: StatItemProps<K>) => (
|
|
25
|
-
<li
|
|
25
|
+
<li
|
|
26
|
+
data-component-id="StatItem"
|
|
27
|
+
className={`stat_box_statistic stat_box_statistic_player_${id}`}
|
|
28
|
+
>
|
|
26
29
|
<span className="stat_box_statistic_name">{label}</span>
|
|
27
30
|
<span className="stat_box_statistic_value">{value}</span>
|
|
28
31
|
</li>
|
package/src/TargetBoost.tsx
CHANGED
package/src/TargetPlayer.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// src/components/TargetPlayer.tsx
|
|
2
|
+
|
|
1
3
|
import { AnimatePresence, motion } from "framer-motion";
|
|
2
4
|
import React, { memo } from "react";
|
|
3
5
|
import { useBroadcast } from "./context/BroadcastContext";
|
|
@@ -9,21 +11,31 @@ export const TargetPlayer = () => {
|
|
|
9
11
|
const broadcast = useBroadcast();
|
|
10
12
|
const targetPlayer = useTargetPlayer();
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
// Safeguard conditions
|
|
15
|
+
if (
|
|
16
|
+
!targetPlayer ||
|
|
17
|
+
!broadcast ||
|
|
18
|
+
!broadcast.teams ||
|
|
19
|
+
!broadcast.teams[targetPlayer.team]
|
|
20
|
+
) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
13
23
|
|
|
14
|
-
const
|
|
24
|
+
const teamData = broadcast.teams[targetPlayer.team];
|
|
25
|
+
const teamColor = teamData?.color?.primary_color ?? "#ffffff";
|
|
15
26
|
const modifier = targetPlayer.team === 0 ? "left" : "right";
|
|
16
27
|
|
|
17
28
|
return (
|
|
18
29
|
<AnimatePresence mode="wait">
|
|
19
30
|
<motion.div
|
|
31
|
+
data-component-id="TargetPlayer"
|
|
20
32
|
key={targetPlayer.id}
|
|
21
33
|
className={`stat_box ${modifier}_stat_box`}
|
|
22
34
|
style={{ "--team-color": teamColor } as React.CSSProperties}
|
|
23
35
|
initial={{ x: modifier === "left" ? -300 : 300, opacity: 0 }}
|
|
24
36
|
animate={{ x: 0, opacity: 1 }}
|
|
25
37
|
exit={{ x: modifier === "left" ? -300 : 300, opacity: 0 }}
|
|
26
|
-
transition={{ duration: 0.8, ease: [0.77, 0, 0.175, 1] }}
|
|
38
|
+
transition={{ duration: 0.8, ease: [0.77, 0, 0.175, 1] }}
|
|
27
39
|
>
|
|
28
40
|
<motion.div
|
|
29
41
|
className={`stat_box_player ${modifier}_stat_box_player`}
|
|
@@ -9,6 +9,7 @@ type Props = {
|
|
|
9
9
|
export const TargetPlayerLocation = ({ location }: Props) => {
|
|
10
10
|
return (
|
|
11
11
|
<motion.div
|
|
12
|
+
data-component-id="TargetPlayerLocation"
|
|
12
13
|
className="stat_box_statistic stat_box_statistic_player_location"
|
|
13
14
|
initial={{ opacity: 0, y: 10 }}
|
|
14
15
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -9,7 +9,7 @@ type Props = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export const TargetPlayerStats = ({ targetPlayer }: Props) => (
|
|
12
|
-
<ul className="stat_box_statistics">
|
|
12
|
+
<ul data-component-id="TargetPlayerStats" className="stat_box_statistics">
|
|
13
13
|
<StatItem id="id" label="ID" value={targetPlayer.id} />
|
|
14
14
|
<StatItem id="team" label="Team" value={targetPlayer.team} />
|
|
15
15
|
<StatItem id="score" label="Score" value={targetPlayer.score} />
|
package/src/Team.tsx
CHANGED
package/src/Timer.tsx
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { memo } from "react";
|
|
2
|
+
import React, { memo } from "react";
|
|
4
3
|
|
|
5
4
|
const TimerComponent: React.FC = () => {
|
|
6
5
|
// Add proper null checks to prevent "Cannot read properties of undefined" error
|
|
7
6
|
const time_seconds = useEventSelector(
|
|
8
7
|
"game:update_state",
|
|
9
|
-
(state) => state?.game?.time_seconds
|
|
8
|
+
(state) => state?.game?.time_seconds,
|
|
10
9
|
);
|
|
11
10
|
|
|
12
11
|
// Return null if time_seconds is not available
|
|
@@ -16,7 +15,11 @@ const TimerComponent: React.FC = () => {
|
|
|
16
15
|
const seconds = time_seconds % 60;
|
|
17
16
|
const formattedTime = `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
|
18
17
|
|
|
19
|
-
return
|
|
18
|
+
return (
|
|
19
|
+
<span data-component-id="Timer" className="time_box">
|
|
20
|
+
{formattedTime}
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
20
23
|
};
|
|
21
24
|
|
|
22
25
|
export const Timer = memo(TimerComponent);
|