@four-leaf-studios/rl-overlay 1.0.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/dist/index.cjs.js +10718 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.esm.js +10691 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/Overlay.d.ts +19 -0
- package/dist/types/Player.d.ts +7 -0
- package/dist/types/PlayerBoost.d.ts +8 -0
- package/dist/types/Replay.d.ts +3 -0
- package/dist/types/Scoreboard.d.ts +3 -0
- package/dist/types/ScoreboardGameBox.d.ts +7 -0
- package/dist/types/ScoreboardSeriesBox.d.ts +7 -0
- package/dist/types/ScoreboardTeam.d.ts +7 -0
- package/dist/types/StatItem.d.ts +13 -0
- package/dist/types/TargetBoost.d.ts +3 -0
- package/dist/types/TargetPlayer.d.ts +3 -0
- package/dist/types/TargetPlayerLocation.d.ts +7 -0
- package/dist/types/TargetPlayerStats.d.ts +7 -0
- package/dist/types/Team.d.ts +8 -0
- package/dist/types/Teams.d.ts +3 -0
- package/dist/types/Timer.d.ts +3 -0
- package/dist/types/context/BroadcastContext.d.ts +7 -0
- package/dist/types/hooks/index.d.ts +4 -0
- package/dist/types/hooks/useOverlayStyles.d.ts +12 -0
- package/dist/types/hooks/useReplay.d.ts +7 -0
- package/dist/types/hooks/useShowGameComponents.d.ts +2 -0
- package/dist/types/hooks/useTargetPlayer.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/mockBroadcast.d.ts +18 -0
- package/package.json +50 -0
- package/rollup.config.js +55 -0
- package/src/Overlay.tsx +100 -0
- package/src/Player.tsx +31 -0
- package/src/PlayerBoost.tsx +26 -0
- package/src/Replay.tsx +18 -0
- package/src/Scoreboard.tsx +58 -0
- package/src/ScoreboardGameBox.tsx +25 -0
- package/src/ScoreboardSeriesBox.tsx +44 -0
- package/src/ScoreboardTeam.tsx +39 -0
- package/src/StatItem.tsx +31 -0
- package/src/TargetBoost.tsx +63 -0
- package/src/TargetPlayer.tsx +56 -0
- package/src/TargetPlayerLocation.tsx +27 -0
- package/src/TargetPlayerStats.tsx +25 -0
- package/src/Team.tsx +45 -0
- package/src/Teams.tsx +13 -0
- package/src/Timer.tsx +24 -0
- package/src/assets/overlay-testing-background.png +0 -0
- package/src/context/BroadcastContext.tsx +21 -0
- package/src/css/reset.css +280 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useOverlayStyles.ts +107 -0
- package/src/hooks/useReplay.ts +21 -0
- package/src/hooks/useShowGameComponents.ts +14 -0
- package/src/hooks/useTargetPlayer.ts +21 -0
- package/src/index.ts +3 -0
- package/src/types.d.ts +59 -0
- package/test-overlay/README.md +12 -0
- package/test-overlay/eslint.config.js +33 -0
- package/test-overlay/index.html +13 -0
- package/test-overlay/package.json +27 -0
- package/test-overlay/public/mock-css.css +733 -0
- package/test-overlay/public/vite.svg +1 -0
- package/test-overlay/src/App.jsx +28 -0
- package/test-overlay/src/index.css +0 -0
- package/test-overlay/src/main.jsx +10 -0
- package/test-overlay/src/mockBroadcast.js +33 -0
- package/test-overlay/vite.config.js +7 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import type { Broadcast } from "./types";
|
|
3
|
+
import { CSSJSON } from "./hooks/useOverlayStyles";
|
|
4
|
+
import "./css/reset.css";
|
|
5
|
+
export type OverlayProps = {
|
|
6
|
+
broadcast: Broadcast;
|
|
7
|
+
styles?: string | CSSJSON;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
preview?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const Overlay: {
|
|
12
|
+
({ broadcast, styles, preview, children, }: OverlayProps): React.JSX.Element;
|
|
13
|
+
Scoreboard: () => React.JSX.Element | null;
|
|
14
|
+
Teams: () => React.JSX.Element;
|
|
15
|
+
TargetPlayer: React.MemoExoticComponent<() => React.JSX.Element | null>;
|
|
16
|
+
TargetBoost: React.MemoExoticComponent<() => React.JSX.Element | null>;
|
|
17
|
+
Replay: () => React.JSX.Element | null;
|
|
18
|
+
};
|
|
19
|
+
export default Overlay;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import React from "react";
|
|
3
|
+
type Props = {
|
|
4
|
+
team: PlayerState["team"];
|
|
5
|
+
boost: PlayerState["boost"];
|
|
6
|
+
};
|
|
7
|
+
declare const _default: React.MemoExoticComponent<({ team, boost }: Props) => React.JSX.Element>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
3
|
+
type Primitive = string | number | boolean;
|
|
4
|
+
type PrimitiveKeys<T> = {
|
|
5
|
+
[K in keyof T]: T[K] extends Primitive ? K : never;
|
|
6
|
+
}[keyof T];
|
|
7
|
+
type StatItemProps<K extends PrimitiveKeys<PlayerState>> = {
|
|
8
|
+
id: K;
|
|
9
|
+
label: string;
|
|
10
|
+
value: PlayerState[K];
|
|
11
|
+
};
|
|
12
|
+
declare const StatItem: <K extends PrimitiveKeys<PlayerState>>({ id, label, value, }: StatItemProps<K>) => React.JSX.Element;
|
|
13
|
+
export default StatItem;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
3
|
+
type Props = {
|
|
4
|
+
location: PlayerState["location"];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: React.MemoExoticComponent<({ location }: Props) => React.JSX.Element>;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
3
|
+
type Props = {
|
|
4
|
+
targetPlayer: PlayerState;
|
|
5
|
+
};
|
|
6
|
+
declare const _default: React.MemoExoticComponent<({ targetPlayer }: Props) => React.JSX.Element>;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Team } from "./types";
|
|
3
|
+
type Props = {
|
|
4
|
+
id: Team["id"];
|
|
5
|
+
};
|
|
6
|
+
declare const Team: ({ id }: Props) => React.JSX.Element | null;
|
|
7
|
+
declare const MemoizedTeam: React.MemoExoticComponent<({ id }: Props) => React.JSX.Element | null>;
|
|
8
|
+
export default MemoizedTeam;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Broadcast } from "../types";
|
|
2
|
+
export type CSSJSON = {
|
|
3
|
+
[selector: string]: {
|
|
4
|
+
[prop: string]: string | number;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Hook that injects both team-color variables (from Broadcast)
|
|
9
|
+
* and optional custom styles (string or JSON) into the document.
|
|
10
|
+
* Uses `@rysh/json-to-css` for JSON → CSS conversion.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useOverlayStyles(broadcast: Broadcast, styles?: string | CSSJSON): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const mockBroadcastData: {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
teams: {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
series_score: number;
|
|
9
|
+
side: string;
|
|
10
|
+
color: {
|
|
11
|
+
primary_color: string;
|
|
12
|
+
secondary_color: string;
|
|
13
|
+
mutual_color: string;
|
|
14
|
+
};
|
|
15
|
+
}[];
|
|
16
|
+
series_number: number;
|
|
17
|
+
top_info_text: string;
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@four-leaf-studios/rl-overlay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.cjs.js",
|
|
5
|
+
"module": "dist/index.esm.js",
|
|
6
|
+
"types": "dist/types/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "restricted"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rollup -c",
|
|
12
|
+
"prepare": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Four-Leaf-Studios/rl-overlay.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Four-Leaf-Studios/rl-overlay/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Four-Leaf-Studios/rl-overlay#readme",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^18 || ^19",
|
|
27
|
+
"react-dom": "^18 || ^19"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@rollup/plugin-commonjs": "^28.0.3",
|
|
31
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
32
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
33
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
34
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
35
|
+
"@rollup/plugin-url": "^8.0.2",
|
|
36
|
+
"@types/postcss-js": "^4.0.4",
|
|
37
|
+
"@types/react": "^19.1.2",
|
|
38
|
+
"postcss-url": "^10.1.3",
|
|
39
|
+
"rollup": "^4.40.1",
|
|
40
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
41
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
42
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
43
|
+
"typescript": "^5.8.3"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@four-leaf-studios/rl-socket-hook": "^1.2.2",
|
|
47
|
+
"@rysh/json-to-css": "^1.0.0",
|
|
48
|
+
"framer-motion": "^12.9.4"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// rollup.config.js
|
|
2
|
+
import peerDepsExternal from "rollup-plugin-peer-deps-external";
|
|
3
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
4
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
5
|
+
import typescript from "@rollup/plugin-typescript";
|
|
6
|
+
import json from "@rollup/plugin-json";
|
|
7
|
+
import postcss from "rollup-plugin-postcss";
|
|
8
|
+
import postcssUrl from "postcss-url";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
input: "src/index.ts",
|
|
12
|
+
output: [
|
|
13
|
+
{
|
|
14
|
+
file: "dist/index.cjs.js",
|
|
15
|
+
format: "cjs",
|
|
16
|
+
exports: "named",
|
|
17
|
+
sourcemap: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
file: "dist/index.esm.js",
|
|
21
|
+
format: "esm",
|
|
22
|
+
sourcemap: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
// suppress the "use client" warning if you want
|
|
27
|
+
onwarn(warning, handler) {
|
|
28
|
+
if (warning.code === "MODULE_LEVEL_DIRECTIVE") return;
|
|
29
|
+
handler(warning);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
plugins: [
|
|
33
|
+
peerDepsExternal(),
|
|
34
|
+
json(),
|
|
35
|
+
|
|
36
|
+
// process CSS and inline any url() assets as base64
|
|
37
|
+
postcss({
|
|
38
|
+
extract: false,
|
|
39
|
+
modules: false,
|
|
40
|
+
minimize: true,
|
|
41
|
+
sourceMap: true,
|
|
42
|
+
plugins: [
|
|
43
|
+
postcssUrl({
|
|
44
|
+
url: "inline", // inline every asset as data-URI
|
|
45
|
+
maxSize: Infinity, // no size limit
|
|
46
|
+
fallback: "copy", // if inline fails, copy file to dist
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
resolve(),
|
|
52
|
+
commonjs(),
|
|
53
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
54
|
+
],
|
|
55
|
+
};
|
package/src/Overlay.tsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/Overlay.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { ReactNode } from "react";
|
|
5
|
+
import { RLProvider, WebsocketData } from "@four-leaf-studios/rl-socket-hook";
|
|
6
|
+
import { BroadcastProvider } from "./context/BroadcastContext";
|
|
7
|
+
import type { Broadcast } from "./types";
|
|
8
|
+
|
|
9
|
+
import Scoreboard from "./Scoreboard";
|
|
10
|
+
import Teams from "./Teams";
|
|
11
|
+
import TargetPlayer from "./TargetPlayer";
|
|
12
|
+
import TargetBoost from "./TargetBoost";
|
|
13
|
+
import Replay from "./Replay";
|
|
14
|
+
|
|
15
|
+
import { useOverlayStyles, CSSJSON } from "./hooks/useOverlayStyles";
|
|
16
|
+
import "./css/reset.css";
|
|
17
|
+
|
|
18
|
+
export type OverlayProps = {
|
|
19
|
+
broadcast: Broadcast;
|
|
20
|
+
styles?: string | CSSJSON;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
preview?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Overlay = ({
|
|
26
|
+
broadcast,
|
|
27
|
+
styles,
|
|
28
|
+
preview,
|
|
29
|
+
children,
|
|
30
|
+
}: OverlayProps) => {
|
|
31
|
+
useOverlayStyles(broadcast, styles);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<BroadcastProvider broadcast={broadcast}>
|
|
35
|
+
<RLProvider>
|
|
36
|
+
<div
|
|
37
|
+
style={{
|
|
38
|
+
display: "flex",
|
|
39
|
+
flexDirection: "row",
|
|
40
|
+
width: "100%",
|
|
41
|
+
height: "100%",
|
|
42
|
+
maxWidth: "100%",
|
|
43
|
+
maxHeight: "100%",
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{/* The overlay itself */}
|
|
47
|
+
<div
|
|
48
|
+
className={`overlay ${preview ? "testing" : ""}`}
|
|
49
|
+
style={{
|
|
50
|
+
position: "relative",
|
|
51
|
+
width: "100%",
|
|
52
|
+
height: "100%",
|
|
53
|
+
aspectRatio: "16/9",
|
|
54
|
+
maxWidth: "1920px",
|
|
55
|
+
maxHeight: "1080px",
|
|
56
|
+
display: "block",
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{children ?? (
|
|
60
|
+
<>
|
|
61
|
+
<Scoreboard />
|
|
62
|
+
<Teams />
|
|
63
|
+
<TargetPlayer />
|
|
64
|
+
<TargetBoost />
|
|
65
|
+
<Replay />
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Preview data panel on the right */}
|
|
71
|
+
{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
|
+
>
|
|
84
|
+
<WebsocketData />
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</RLProvider>
|
|
89
|
+
</BroadcastProvider>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// expose slots
|
|
94
|
+
Overlay.Scoreboard = Scoreboard;
|
|
95
|
+
Overlay.Teams = Teams;
|
|
96
|
+
Overlay.TargetPlayer = TargetPlayer;
|
|
97
|
+
Overlay.TargetBoost = TargetBoost;
|
|
98
|
+
Overlay.Replay = Replay;
|
|
99
|
+
|
|
100
|
+
export default Overlay;
|
package/src/Player.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { memo, useRef } from "react";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
4
|
+
import PlayerBoost from "./PlayerBoost";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
player: PlayerState;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const Player = ({ player }: Props) => {
|
|
11
|
+
const modifier = player.team === 0 ? "left" : "right";
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<motion.div
|
|
15
|
+
key={player.id}
|
|
16
|
+
className={`player_box ${modifier}_player_box`}
|
|
17
|
+
initial={{ opacity: 0, y: 20 }}
|
|
18
|
+
animate={{ opacity: 1, y: 0 }}
|
|
19
|
+
exit={{ opacity: 0, y: 20 }}
|
|
20
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
21
|
+
>
|
|
22
|
+
<div className={`player_boost ${modifier}_player_boost`}>
|
|
23
|
+
{player.boost}
|
|
24
|
+
</div>
|
|
25
|
+
<div className={`player_name ${modifier}_player_name`}>{player.name}</div>
|
|
26
|
+
<PlayerBoost key={player.id} boost={player.boost} team={player.team} />
|
|
27
|
+
</motion.div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default memo(Player);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import React, { memo } from "react";
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
team: PlayerState["team"];
|
|
7
|
+
boost: PlayerState["boost"];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const PlayerBoost = ({ team, boost }: Props) => {
|
|
11
|
+
const boostPercent = Math.min(Math.max(boost, 0), 100);
|
|
12
|
+
const modifier = team === 0 ? "left" : "right";
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={`boost_meter ${modifier}_boost_meter`}>
|
|
16
|
+
<motion.div
|
|
17
|
+
className={`boost_meter_bar ${modifier}_boost_meter_bar`}
|
|
18
|
+
initial={{ width: "0%" }}
|
|
19
|
+
animate={{ width: `${boostPercent}%` }}
|
|
20
|
+
transition={{ duration: 0.5, ease: "easeInOut" }}
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default memo(PlayerBoost);
|
package/src/Replay.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import useReplay from "./hooks/useReplay";
|
|
3
|
+
|
|
4
|
+
const Replay = () => {
|
|
5
|
+
const { active: replayActive } = useReplay();
|
|
6
|
+
|
|
7
|
+
if (!replayActive) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="replay_box">
|
|
13
|
+
<div className="replay_text">Replay</div>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default Replay;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import useShowGameComponents from "./hooks/useShowGameComponents";
|
|
4
|
+
import { useBroadcast } from "./context/BroadcastContext";
|
|
5
|
+
import ScoreboardTeam from "./ScoreboardTeam";
|
|
6
|
+
import Timer from "./Timer";
|
|
7
|
+
import ScoreboardSeriesBox from "./ScoreboardSeriesBox";
|
|
8
|
+
import ScoreboardGameBox from "./ScoreboardGameBox";
|
|
9
|
+
import useReplay from "./hooks/useReplay";
|
|
10
|
+
|
|
11
|
+
const Scoreboard = () => {
|
|
12
|
+
const broadcast = useBroadcast();
|
|
13
|
+
const showGameComponents = useShowGameComponents();
|
|
14
|
+
const { active: replayActive } = useReplay();
|
|
15
|
+
|
|
16
|
+
if (!broadcast || !broadcast.teams || broadcast.teams.length < 2) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!showGameComponents && replayActive) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<AnimatePresence mode="wait">
|
|
24
|
+
<motion.div
|
|
25
|
+
key={broadcast.id} // Ensures animation runs when broadcast updates
|
|
26
|
+
className="scoreboard_box"
|
|
27
|
+
initial={{ y: -100, opacity: 0 }}
|
|
28
|
+
animate={{ y: 0, opacity: 1 }}
|
|
29
|
+
exit={{ y: -100, opacity: 0 }}
|
|
30
|
+
transition={{ duration: 0.8, ease: [0.68, -0.55, 0.27, 1.55] }} // Mimics Svelte backInOut
|
|
31
|
+
>
|
|
32
|
+
{broadcast?.top_info_text && (
|
|
33
|
+
<div className="top_bar">{broadcast.top_info_text}</div>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
<div className="main_bar">
|
|
37
|
+
{broadcast.teams.map((team: any, idx: number) => (
|
|
38
|
+
<ScoreboardTeam key={team.id || idx} team={team} />
|
|
39
|
+
))}
|
|
40
|
+
<Timer />
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="bottom_bar">
|
|
44
|
+
{broadcast.teams.map((team: any, idx: number) => (
|
|
45
|
+
<ScoreboardSeriesBox
|
|
46
|
+
key={team.id || idx}
|
|
47
|
+
team={team}
|
|
48
|
+
seriesNumber={broadcast.series_number}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
<ScoreboardGameBox broadcast={broadcast} />
|
|
52
|
+
</div>
|
|
53
|
+
</motion.div>
|
|
54
|
+
</AnimatePresence>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default Scoreboard;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Broadcast } from "./types";
|
|
5
|
+
|
|
6
|
+
interface ScoreboardGameBoxProps {
|
|
7
|
+
broadcast: Broadcast;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ScoreboardGameBox: React.FC<ScoreboardGameBoxProps> = ({ broadcast }) => {
|
|
11
|
+
const gameNumber = useMemo(() => {
|
|
12
|
+
if (!broadcast.teams || broadcast.teams.length < 2) return 1;
|
|
13
|
+
const team0Score = broadcast.teams[0].series_score || 0;
|
|
14
|
+
const team1Score = broadcast.teams[1].series_score || 0;
|
|
15
|
+
return team0Score + team1Score + 1;
|
|
16
|
+
}, [broadcast.teams]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="game_box">
|
|
20
|
+
Game {gameNumber} - BO {broadcast.series_number}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default ScoreboardGameBox;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
|
|
5
|
+
interface ScoreboardSeriesBoxProps {
|
|
6
|
+
team: any;
|
|
7
|
+
seriesNumber: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ScoreboardSeriesBoxComponent: React.FC<ScoreboardSeriesBoxProps> = ({
|
|
11
|
+
team,
|
|
12
|
+
seriesNumber,
|
|
13
|
+
}) => {
|
|
14
|
+
if (!seriesNumber) return null;
|
|
15
|
+
|
|
16
|
+
const bestOf = Number(seriesNumber);
|
|
17
|
+
const teamSeriesScore = Number(team.series_score) || 0;
|
|
18
|
+
const pointsCount = Math.ceil(bestOf / 2);
|
|
19
|
+
const points = Array.from({ length: pointsCount }, (_, index) => index);
|
|
20
|
+
|
|
21
|
+
const isLefTeam = team.id === 0;
|
|
22
|
+
const modifier = isLefTeam ? "left" : "right";
|
|
23
|
+
const teamColor =
|
|
24
|
+
team.color?.primary_color || (isLefTeam ? "#0052cc" : "#ff6600");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={`series_box ${modifier}_series_box`}
|
|
29
|
+
style={{ "--team-color": teamColor } as React.CSSProperties}
|
|
30
|
+
>
|
|
31
|
+
{points.map((index) => {
|
|
32
|
+
const pointClass =
|
|
33
|
+
index < teamSeriesScore
|
|
34
|
+
? `series_score_box_point ${modifier}_series_score_box_point`
|
|
35
|
+
: `series_score_box_empty ${modifier}_series_score_box_empty`;
|
|
36
|
+
return <div key={index} className={`series_score_box ${pointClass}`} />;
|
|
37
|
+
})}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ScoreboardSeriesBox = memo(ScoreboardSeriesBoxComponent);
|
|
43
|
+
ScoreboardSeriesBox.displayName = "ScoreboardSeriesBox";
|
|
44
|
+
export default ScoreboardSeriesBox;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Team } from "./types";
|
|
4
|
+
|
|
5
|
+
interface ScoreboardTeamProps {
|
|
6
|
+
team: Team;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ScoreboardTeam: React.FC<ScoreboardTeamProps> = ({ team }) => {
|
|
10
|
+
const score = useEventSelector(
|
|
11
|
+
"game:update_state",
|
|
12
|
+
(state) => state?.game.teams?.[team.id]?.score
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const isLeftTeam = team.id === "0";
|
|
16
|
+
const modifier = isLeftTeam ? "left" : "right";
|
|
17
|
+
const teamColor = team?.color?.primary_color;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={`scoreboard_team_box ${modifier}_scoreboard_team_box`}
|
|
22
|
+
style={{ "--team-color": teamColor } as React.CSSProperties}
|
|
23
|
+
>
|
|
24
|
+
<div className={`name_box ${modifier}_name_box`}>
|
|
25
|
+
{team?.name || (isLeftTeam ? "Blue Team" : "Orange Team")}
|
|
26
|
+
</div>
|
|
27
|
+
{team?.logo && (
|
|
28
|
+
<img
|
|
29
|
+
className={`logo_box ${modifier}_logo_box`}
|
|
30
|
+
src={team.logo || "/placeholder.svg"}
|
|
31
|
+
alt={team.name}
|
|
32
|
+
/>
|
|
33
|
+
)}
|
|
34
|
+
<div className={`score_box ${modifier}_score_box`}>{score}</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default ScoreboardTeam;
|