@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.
Files changed (68) hide show
  1. package/dist/index.cjs.js +10718 -0
  2. package/dist/index.cjs.js.map +1 -0
  3. package/dist/index.esm.js +10691 -0
  4. package/dist/index.esm.js.map +1 -0
  5. package/dist/types/Overlay.d.ts +19 -0
  6. package/dist/types/Player.d.ts +7 -0
  7. package/dist/types/PlayerBoost.d.ts +8 -0
  8. package/dist/types/Replay.d.ts +3 -0
  9. package/dist/types/Scoreboard.d.ts +3 -0
  10. package/dist/types/ScoreboardGameBox.d.ts +7 -0
  11. package/dist/types/ScoreboardSeriesBox.d.ts +7 -0
  12. package/dist/types/ScoreboardTeam.d.ts +7 -0
  13. package/dist/types/StatItem.d.ts +13 -0
  14. package/dist/types/TargetBoost.d.ts +3 -0
  15. package/dist/types/TargetPlayer.d.ts +3 -0
  16. package/dist/types/TargetPlayerLocation.d.ts +7 -0
  17. package/dist/types/TargetPlayerStats.d.ts +7 -0
  18. package/dist/types/Team.d.ts +8 -0
  19. package/dist/types/Teams.d.ts +3 -0
  20. package/dist/types/Timer.d.ts +3 -0
  21. package/dist/types/context/BroadcastContext.d.ts +7 -0
  22. package/dist/types/hooks/index.d.ts +4 -0
  23. package/dist/types/hooks/useOverlayStyles.d.ts +12 -0
  24. package/dist/types/hooks/useReplay.d.ts +7 -0
  25. package/dist/types/hooks/useShowGameComponents.d.ts +2 -0
  26. package/dist/types/hooks/useTargetPlayer.d.ts +2 -0
  27. package/dist/types/index.d.ts +3 -0
  28. package/dist/types/mockBroadcast.d.ts +18 -0
  29. package/package.json +50 -0
  30. package/rollup.config.js +55 -0
  31. package/src/Overlay.tsx +100 -0
  32. package/src/Player.tsx +31 -0
  33. package/src/PlayerBoost.tsx +26 -0
  34. package/src/Replay.tsx +18 -0
  35. package/src/Scoreboard.tsx +58 -0
  36. package/src/ScoreboardGameBox.tsx +25 -0
  37. package/src/ScoreboardSeriesBox.tsx +44 -0
  38. package/src/ScoreboardTeam.tsx +39 -0
  39. package/src/StatItem.tsx +31 -0
  40. package/src/TargetBoost.tsx +63 -0
  41. package/src/TargetPlayer.tsx +56 -0
  42. package/src/TargetPlayerLocation.tsx +27 -0
  43. package/src/TargetPlayerStats.tsx +25 -0
  44. package/src/Team.tsx +45 -0
  45. package/src/Teams.tsx +13 -0
  46. package/src/Timer.tsx +24 -0
  47. package/src/assets/overlay-testing-background.png +0 -0
  48. package/src/context/BroadcastContext.tsx +21 -0
  49. package/src/css/reset.css +280 -0
  50. package/src/hooks/index.ts +4 -0
  51. package/src/hooks/useOverlayStyles.ts +107 -0
  52. package/src/hooks/useReplay.ts +21 -0
  53. package/src/hooks/useShowGameComponents.ts +14 -0
  54. package/src/hooks/useTargetPlayer.ts +21 -0
  55. package/src/index.ts +3 -0
  56. package/src/types.d.ts +59 -0
  57. package/test-overlay/README.md +12 -0
  58. package/test-overlay/eslint.config.js +33 -0
  59. package/test-overlay/index.html +13 -0
  60. package/test-overlay/package.json +27 -0
  61. package/test-overlay/public/mock-css.css +733 -0
  62. package/test-overlay/public/vite.svg +1 -0
  63. package/test-overlay/src/App.jsx +28 -0
  64. package/test-overlay/src/index.css +0 -0
  65. package/test-overlay/src/main.jsx +10 -0
  66. package/test-overlay/src/mockBroadcast.js +33 -0
  67. package/test-overlay/vite.config.js +7 -0
  68. 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,7 @@
1
+ import React from "react";
2
+ import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
3
+ type Props = {
4
+ player: PlayerState;
5
+ };
6
+ declare const _default: React.MemoExoticComponent<({ player }: Props) => React.JSX.Element>;
7
+ export default _default;
@@ -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,3 @@
1
+ import React from "react";
2
+ declare const Replay: () => React.JSX.Element | null;
3
+ export default Replay;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ declare const Scoreboard: () => React.JSX.Element | null;
3
+ export default Scoreboard;
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { Broadcast } from "./types";
3
+ interface ScoreboardGameBoxProps {
4
+ broadcast: Broadcast;
5
+ }
6
+ declare const ScoreboardGameBox: React.FC<ScoreboardGameBoxProps>;
7
+ export default ScoreboardGameBox;
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ interface ScoreboardSeriesBoxProps {
3
+ team: any;
4
+ seriesNumber: number;
5
+ }
6
+ declare const ScoreboardSeriesBox: React.NamedExoticComponent<ScoreboardSeriesBoxProps>;
7
+ export default ScoreboardSeriesBox;
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { Team } from "./types";
3
+ interface ScoreboardTeamProps {
4
+ team: Team;
5
+ }
6
+ declare const ScoreboardTeam: React.FC<ScoreboardTeamProps>;
7
+ export default ScoreboardTeam;
@@ -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,3 @@
1
+ import React from "react";
2
+ declare const _default: React.MemoExoticComponent<() => React.JSX.Element | null>;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ declare const _default: React.MemoExoticComponent<() => React.JSX.Element | null>;
3
+ 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
+ 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,3 @@
1
+ import React from "react";
2
+ declare const Teams: () => React.JSX.Element;
3
+ export default Teams;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ declare const Timer: React.NamedExoticComponent<{}>;
3
+ export default Timer;
@@ -0,0 +1,7 @@
1
+ import React, { ReactNode } from "react";
2
+ import { Broadcast } from "../types";
3
+ export declare const BroadcastProvider: React.FC<{
4
+ broadcast: Broadcast;
5
+ children: ReactNode;
6
+ }>;
7
+ export declare function useBroadcast(): Broadcast;
@@ -0,0 +1,4 @@
1
+ export * from "./useOverlayStyles";
2
+ export * from "./useReplay";
3
+ export * from "./useShowGameComponents";
4
+ export * from "./useTargetPlayer";
@@ -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,7 @@
1
+ declare const useReplay: () => {
2
+ active: boolean;
3
+ replayEndEvent: string | undefined;
4
+ replayStartEvent: string | undefined;
5
+ isReplay: boolean | undefined;
6
+ };
7
+ export default useReplay;
@@ -0,0 +1,2 @@
1
+ declare const useShowGameComponents: () => boolean;
2
+ export default useShowGameComponents;
@@ -0,0 +1,2 @@
1
+ declare const useTargetPlayer: () => import("@four-leaf-studios/rl-socket-hook").PlayerState | null | undefined;
2
+ export default useTargetPlayer;
@@ -0,0 +1,3 @@
1
+ export * from "./Overlay";
2
+ export * from "@four-leaf-studios/rl-socket-hook";
3
+ export * from "./hooks/index";
@@ -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
+ }
@@ -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
+ };
@@ -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;