bracket-pro-react 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/README.md +105 -0
- package/bracket-styles.css +204 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +325 -0
- package/dist/index.mjs +286 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
|
|
2
|
+
# Bracket Pro React v2.5
|
|
3
|
+
|
|
4
|
+
A sophisticated, highly customizable tournament bracket engine for React and Next.js. Designed for high-performance e-sports platforms, sports management, and gaming communities.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## 🚀 Features
|
|
11
|
+
|
|
12
|
+
- **Double & Single Elimination**: Full support for standard formats and variations (No Comeback, No Secondary Final).
|
|
13
|
+
- **Deep Design Authority**: Customize every dimension—card width, gaps, border-radius, and colors—via the `theme` prop.
|
|
14
|
+
- **Framework Independent**: Works perfectly with Tailwind, Bootstrap, or pure CSS. No utility class collisions.
|
|
15
|
+
- **Match Reporting**: Built-in interactive reporting modal supporting Best of 1, 3, or 5 matches.
|
|
16
|
+
- **Compressed Mode**: High-density view for large brackets, optimized for mobile responsiveness.
|
|
17
|
+
- **TypeScript First**: Full type safety for tournament data and theme configurations.
|
|
18
|
+
|
|
19
|
+
## 📦 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install bracket-pro-react
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 🛠️ Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Import Styles (Crucial Step)
|
|
28
|
+
Unlike plain HTML where you use `<link>`, in React you must import the CSS file directly into your JavaScript/TypeScript entry point (usually `App.tsx` or `main.tsx`). This allows your build tool to bundle the styles correctly.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// At the very top of your App.tsx
|
|
32
|
+
import 'bracket-pro-react/bracket-styles.css';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Basic Usage
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { TournamentBracket, BracketData } from 'bracket-pro-react';
|
|
39
|
+
|
|
40
|
+
const myData: BracketData = {
|
|
41
|
+
type: 'single',
|
|
42
|
+
rounds: [
|
|
43
|
+
{
|
|
44
|
+
id: 'r1',
|
|
45
|
+
title: 'Quarter Finals',
|
|
46
|
+
matches: [
|
|
47
|
+
{
|
|
48
|
+
id: 'm1',
|
|
49
|
+
participants: [
|
|
50
|
+
{ id: '1', name: 'Alpha Team', score: 2, status: 'winner' },
|
|
51
|
+
{ id: '2', name: 'Beta Squad', score: 0 }
|
|
52
|
+
],
|
|
53
|
+
bestOf: 3
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default function MyBracket() {
|
|
61
|
+
return (
|
|
62
|
+
<TournamentBracket
|
|
63
|
+
data={myData}
|
|
64
|
+
theme={{
|
|
65
|
+
accentColor: '#6366f1',
|
|
66
|
+
cardWidth: 240,
|
|
67
|
+
borderRadius: 12
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 🎨 Customization (Theming)
|
|
75
|
+
|
|
76
|
+
The `theme` prop gives you full control over the bracket's aesthetics. It uses CSS variables internally to ensure zero performance overhead.
|
|
77
|
+
|
|
78
|
+
| Property | Type | Default | Description |
|
|
79
|
+
| :--- | :--- | :--- | :--- |
|
|
80
|
+
| `cardWidth` | `number \| string` | `210px` | The width of individual match cards. |
|
|
81
|
+
| `roundGap` | `number \| string` | `60px` | Horizontal spacing between tournament rounds. |
|
|
82
|
+
| `matchGap` | `number \| string` | `32px` | Vertical spacing between matches in a round. |
|
|
83
|
+
| `accentColor` | `string` | `#6366f1` | Primary theme color (lines, active states). |
|
|
84
|
+
| `cardBackground`| `string` | `#0f172a` | Background color of the match card. |
|
|
85
|
+
| `borderRadius` | `number` | `12` | Corner radius for cards and UI elements. |
|
|
86
|
+
| `fontSize` | `number` | `12` | Base font size for team names and scores. |
|
|
87
|
+
|
|
88
|
+
## ⚙️ Advanced Configuration
|
|
89
|
+
|
|
90
|
+
### Double Elimination Variations
|
|
91
|
+
Control specific tournament rules via the `config` object inside your data:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const config = {
|
|
95
|
+
noSecondaryFinal: true, // No "bracket reset" if Losers winner beats Winners winner.
|
|
96
|
+
noComeback: true, // Losers bracket stops early (3rd place decider).
|
|
97
|
+
autoMobileCompression: true // Automatically switch to high-density view on small screens.
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 📱 Mobile Support
|
|
102
|
+
Bracket Pro is built to be responsive. When screen size is detected as mobile, the bracket can automatically switch to **Compressed Mode**, showing team codes and minimizing padding to ensure the tournament remains legible on small devices.
|
|
103
|
+
|
|
104
|
+
## 📄 License
|
|
105
|
+
MIT © [Your Name]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
|
|
2
|
+
:root {
|
|
3
|
+
/* Default Theme Variables - Can be overridden by the theme prop */
|
|
4
|
+
--bp-bg: transparent;
|
|
5
|
+
--bp-card-bg: #0f172a;
|
|
6
|
+
--bp-border: rgba(255, 255, 255, 0.1);
|
|
7
|
+
--bp-accent: #6366f1;
|
|
8
|
+
--bp-text: #f8fafc;
|
|
9
|
+
--bp-text-muted: #64748b;
|
|
10
|
+
--bp-winner-bg: rgba(99, 102, 241, 0.1);
|
|
11
|
+
--bp-winner-text: #818cf8;
|
|
12
|
+
|
|
13
|
+
/* Default Layout Variables */
|
|
14
|
+
--bp-card-width: 210px;
|
|
15
|
+
--bp-round-gap: 60px;
|
|
16
|
+
--bp-match-gap: 32px;
|
|
17
|
+
--bp-border-radius: 12px;
|
|
18
|
+
--bp-font-size: 12px;
|
|
19
|
+
--bp-font-family: 'Inter', sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.bp-container {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: 80px;
|
|
26
|
+
padding: 40px;
|
|
27
|
+
overflow: auto;
|
|
28
|
+
user-select: none;
|
|
29
|
+
font-family: var(--bp-font-family);
|
|
30
|
+
background-color: var(--bp-bg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.bp-section {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
gap: 40px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.bp-section-header {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 16px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bp-section-title {
|
|
46
|
+
font-size: calc(var(--bp-font-size) * 0.85);
|
|
47
|
+
font-weight: 900;
|
|
48
|
+
text-transform: uppercase;
|
|
49
|
+
letter-spacing: 0.3em;
|
|
50
|
+
color: var(--bp-accent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.bp-section-line {
|
|
54
|
+
height: 1px;
|
|
55
|
+
width: 100%;
|
|
56
|
+
background: linear-gradient(90deg, var(--bp-accent), transparent);
|
|
57
|
+
opacity: 0.2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.bp-rounds-wrapper {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: var(--bp-round-gap);
|
|
63
|
+
align-items: flex-start;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bp-round-column {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: var(--bp-match-gap);
|
|
70
|
+
min-width: max-content;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.bp-round-title-box {
|
|
74
|
+
padding: 6px 12px;
|
|
75
|
+
background: rgba(255, 255, 255, 0.05);
|
|
76
|
+
border-radius: calc(var(--bp-border-radius) / 2);
|
|
77
|
+
border: 1px solid var(--bp-border);
|
|
78
|
+
text-align: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.bp-round-title {
|
|
82
|
+
margin: 0;
|
|
83
|
+
font-size: calc(var(--bp-font-size) * 0.8);
|
|
84
|
+
font-weight: 900;
|
|
85
|
+
text-transform: uppercase;
|
|
86
|
+
color: var(--bp-text-muted);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.bp-matches-list {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
justify-content: space-around;
|
|
93
|
+
height: 100%;
|
|
94
|
+
gap: var(--bp-match-gap);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Match Card Styles */
|
|
98
|
+
.bp-match-card {
|
|
99
|
+
position: relative;
|
|
100
|
+
width: var(--bp-card-width);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
transition: transform 0.2s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.bp-match-card:active {
|
|
106
|
+
transform: scale(0.97);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.bp-match-card-inner {
|
|
110
|
+
background-color: var(--bp-card-bg);
|
|
111
|
+
color: var(--bp-text);
|
|
112
|
+
border-radius: var(--bp-border-radius);
|
|
113
|
+
border: 1px solid var(--bp-border);
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
116
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.bp-match-card:hover .bp-match-card-inner {
|
|
120
|
+
border-color: var(--bp-accent);
|
|
121
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.bp-participant {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: space-between;
|
|
128
|
+
padding: 10px 14px;
|
|
129
|
+
font-size: var(--bp-font-size);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.bp-participant.is-winner {
|
|
133
|
+
background: var(--bp-winner-bg);
|
|
134
|
+
color: var(--bp-winner-text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.bp-participant:first-child {
|
|
138
|
+
border-bottom: 1px solid var(--bp-border);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.bp-team-name {
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
text-transform: uppercase;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.bp-participant-score {
|
|
147
|
+
font-family: monospace;
|
|
148
|
+
font-weight: 900;
|
|
149
|
+
opacity: 0.5;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.bp-participant.is-winner .bp-participant-score {
|
|
153
|
+
opacity: 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.bp-time-tag {
|
|
157
|
+
background: var(--bp-card-bg);
|
|
158
|
+
padding: 2px 8px;
|
|
159
|
+
border: 1px solid var(--bp-border);
|
|
160
|
+
border-radius: 100px;
|
|
161
|
+
font-size: calc(var(--bp-font-size) * 0.6);
|
|
162
|
+
font-weight: 900;
|
|
163
|
+
color: var(--bp-text-muted);
|
|
164
|
+
text-transform: uppercase;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Compressed Mode */
|
|
168
|
+
.bp-match-card.is-compressed {
|
|
169
|
+
--bp-card-width: 120px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.bp-match-card.is-compressed .bp-team-name {
|
|
173
|
+
font-size: calc(var(--bp-font-size) * 0.8);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Modal UI */
|
|
177
|
+
.bp-modal-overlay {
|
|
178
|
+
position: fixed;
|
|
179
|
+
inset: 0;
|
|
180
|
+
z-index: 1000;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
background: rgba(0, 0, 0, 0.8);
|
|
185
|
+
backdrop-filter: blur(4px);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.bp-modal-content {
|
|
189
|
+
background: var(--bp-card-bg);
|
|
190
|
+
color: var(--bp-text);
|
|
191
|
+
border: 1px solid var(--bp-border);
|
|
192
|
+
border-radius: var(--bp-border-radius);
|
|
193
|
+
width: 90%;
|
|
194
|
+
max-width: 500px;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Mobile Adjustments */
|
|
199
|
+
@media (max-width: 768px) {
|
|
200
|
+
.bp-container {
|
|
201
|
+
padding: 16px;
|
|
202
|
+
gap: 40px;
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface GameResult {
|
|
4
|
+
score1: number;
|
|
5
|
+
score2: number;
|
|
6
|
+
}
|
|
7
|
+
interface Participant {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
status?: 'winner' | 'loser' | 'tbd';
|
|
12
|
+
score?: number;
|
|
13
|
+
picture?: string;
|
|
14
|
+
seed?: number;
|
|
15
|
+
}
|
|
16
|
+
interface Match {
|
|
17
|
+
id: string;
|
|
18
|
+
participants: [Participant, Participant];
|
|
19
|
+
startTime?: string;
|
|
20
|
+
bestOf?: number;
|
|
21
|
+
games?: GameResult[];
|
|
22
|
+
nextMatchId?: string;
|
|
23
|
+
nextMatchSlot?: 0 | 1;
|
|
24
|
+
isGrandFinalReset?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface Round {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
matches: Match[];
|
|
30
|
+
}
|
|
31
|
+
type TournamentType = 'single' | 'double';
|
|
32
|
+
interface TournamentConfig {
|
|
33
|
+
noSecondaryFinal?: boolean;
|
|
34
|
+
noComeback?: boolean;
|
|
35
|
+
autoMobileCompression?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface BracketData {
|
|
38
|
+
type: TournamentType;
|
|
39
|
+
rounds: Round[];
|
|
40
|
+
losersRounds?: Round[];
|
|
41
|
+
grandFinal?: Match;
|
|
42
|
+
grandFinalReset?: Match;
|
|
43
|
+
config?: TournamentConfig;
|
|
44
|
+
}
|
|
45
|
+
interface BracketTheme {
|
|
46
|
+
background?: string;
|
|
47
|
+
cardBackground?: string;
|
|
48
|
+
accentColor?: string;
|
|
49
|
+
textColor?: string;
|
|
50
|
+
textColorMuted?: string;
|
|
51
|
+
borderColor?: string;
|
|
52
|
+
winnerBackground?: string;
|
|
53
|
+
winnerTextColor?: string;
|
|
54
|
+
cardWidth?: string | number;
|
|
55
|
+
cardHeight?: string | number;
|
|
56
|
+
roundGap?: string | number;
|
|
57
|
+
matchGap?: string | number;
|
|
58
|
+
borderRadius?: string | number;
|
|
59
|
+
fontSize?: string | number;
|
|
60
|
+
fontFamily?: string;
|
|
61
|
+
}
|
|
62
|
+
type ViewMode = 'compressed' | 'detailed';
|
|
63
|
+
|
|
64
|
+
interface TournamentBracketProps {
|
|
65
|
+
data: BracketData;
|
|
66
|
+
onUpdateMatch?: (match: Match) => void;
|
|
67
|
+
theme?: BracketTheme;
|
|
68
|
+
mode?: ViewMode;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
declare const TournamentBracket: React.FC<TournamentBracketProps>;
|
|
72
|
+
|
|
73
|
+
interface BracketMatchProps {
|
|
74
|
+
match: Match;
|
|
75
|
+
onMatchClick?: (match: Match) => void;
|
|
76
|
+
mode?: ViewMode;
|
|
77
|
+
}
|
|
78
|
+
declare const BracketMatch: React.FC<BracketMatchProps>;
|
|
79
|
+
|
|
80
|
+
interface MatchDetailModalProps {
|
|
81
|
+
match: Match;
|
|
82
|
+
isOpen: boolean;
|
|
83
|
+
onClose: () => void;
|
|
84
|
+
onSave: (match: Match) => void;
|
|
85
|
+
}
|
|
86
|
+
declare const MatchDetailModal: React.FC<MatchDetailModalProps>;
|
|
87
|
+
|
|
88
|
+
export { type BracketData, BracketMatch, type BracketTheme, type GameResult, type Match, MatchDetailModal, type Participant, type Round, TournamentBracket, type TournamentConfig, type TournamentType, type ViewMode };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface GameResult {
|
|
4
|
+
score1: number;
|
|
5
|
+
score2: number;
|
|
6
|
+
}
|
|
7
|
+
interface Participant {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
status?: 'winner' | 'loser' | 'tbd';
|
|
12
|
+
score?: number;
|
|
13
|
+
picture?: string;
|
|
14
|
+
seed?: number;
|
|
15
|
+
}
|
|
16
|
+
interface Match {
|
|
17
|
+
id: string;
|
|
18
|
+
participants: [Participant, Participant];
|
|
19
|
+
startTime?: string;
|
|
20
|
+
bestOf?: number;
|
|
21
|
+
games?: GameResult[];
|
|
22
|
+
nextMatchId?: string;
|
|
23
|
+
nextMatchSlot?: 0 | 1;
|
|
24
|
+
isGrandFinalReset?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface Round {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
matches: Match[];
|
|
30
|
+
}
|
|
31
|
+
type TournamentType = 'single' | 'double';
|
|
32
|
+
interface TournamentConfig {
|
|
33
|
+
noSecondaryFinal?: boolean;
|
|
34
|
+
noComeback?: boolean;
|
|
35
|
+
autoMobileCompression?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface BracketData {
|
|
38
|
+
type: TournamentType;
|
|
39
|
+
rounds: Round[];
|
|
40
|
+
losersRounds?: Round[];
|
|
41
|
+
grandFinal?: Match;
|
|
42
|
+
grandFinalReset?: Match;
|
|
43
|
+
config?: TournamentConfig;
|
|
44
|
+
}
|
|
45
|
+
interface BracketTheme {
|
|
46
|
+
background?: string;
|
|
47
|
+
cardBackground?: string;
|
|
48
|
+
accentColor?: string;
|
|
49
|
+
textColor?: string;
|
|
50
|
+
textColorMuted?: string;
|
|
51
|
+
borderColor?: string;
|
|
52
|
+
winnerBackground?: string;
|
|
53
|
+
winnerTextColor?: string;
|
|
54
|
+
cardWidth?: string | number;
|
|
55
|
+
cardHeight?: string | number;
|
|
56
|
+
roundGap?: string | number;
|
|
57
|
+
matchGap?: string | number;
|
|
58
|
+
borderRadius?: string | number;
|
|
59
|
+
fontSize?: string | number;
|
|
60
|
+
fontFamily?: string;
|
|
61
|
+
}
|
|
62
|
+
type ViewMode = 'compressed' | 'detailed';
|
|
63
|
+
|
|
64
|
+
interface TournamentBracketProps {
|
|
65
|
+
data: BracketData;
|
|
66
|
+
onUpdateMatch?: (match: Match) => void;
|
|
67
|
+
theme?: BracketTheme;
|
|
68
|
+
mode?: ViewMode;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
declare const TournamentBracket: React.FC<TournamentBracketProps>;
|
|
72
|
+
|
|
73
|
+
interface BracketMatchProps {
|
|
74
|
+
match: Match;
|
|
75
|
+
onMatchClick?: (match: Match) => void;
|
|
76
|
+
mode?: ViewMode;
|
|
77
|
+
}
|
|
78
|
+
declare const BracketMatch: React.FC<BracketMatchProps>;
|
|
79
|
+
|
|
80
|
+
interface MatchDetailModalProps {
|
|
81
|
+
match: Match;
|
|
82
|
+
isOpen: boolean;
|
|
83
|
+
onClose: () => void;
|
|
84
|
+
onSave: (match: Match) => void;
|
|
85
|
+
}
|
|
86
|
+
declare const MatchDetailModal: React.FC<MatchDetailModalProps>;
|
|
87
|
+
|
|
88
|
+
export { type BracketData, BracketMatch, type BracketTheme, type GameResult, type Match, MatchDetailModal, type Participant, type Round, TournamentBracket, type TournamentConfig, type TournamentType, type ViewMode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// index.tsx
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BracketMatch: () => BracketMatch,
|
|
34
|
+
MatchDetailModal: () => MatchDetailModal,
|
|
35
|
+
TournamentBracket: () => TournamentBracket
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// components/TournamentBracket.tsx
|
|
40
|
+
var import_react3 = require("react");
|
|
41
|
+
|
|
42
|
+
// components/BracketMatch.tsx
|
|
43
|
+
var import_react = __toESM(require("react"));
|
|
44
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
45
|
+
var BracketMatch = ({
|
|
46
|
+
match,
|
|
47
|
+
onMatchClick,
|
|
48
|
+
mode = "detailed"
|
|
49
|
+
}) => {
|
|
50
|
+
const isCompressed = mode === "compressed";
|
|
51
|
+
const renderParticipant = (p) => {
|
|
52
|
+
const isWinner = p.status === "winner";
|
|
53
|
+
const isTBD = !p.name || p.name === "TBD" || p.name.includes("---");
|
|
54
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
55
|
+
"div",
|
|
56
|
+
{
|
|
57
|
+
className: `bp-participant ${isWinner ? "is-winner" : ""} ${isTBD ? "is-tbd" : ""}`,
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", overflow: "hidden" }, children: [
|
|
60
|
+
!isCompressed && p.picture && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
61
|
+
"img",
|
|
62
|
+
{
|
|
63
|
+
src: p.picture,
|
|
64
|
+
style: { width: "20px", height: "20px", borderRadius: "50%", objectFit: "cover" },
|
|
65
|
+
alt: ""
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "bp-team-name", children: isCompressed ? p.code || p.name.substring(0, 3) : p.name || "TBD" })
|
|
69
|
+
] }),
|
|
70
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "bp-participant-score", children: p.score ?? "-" })
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
76
|
+
"div",
|
|
77
|
+
{
|
|
78
|
+
className: `bp-match-card ${isCompressed ? "is-compressed" : ""}`,
|
|
79
|
+
onClick: () => onMatchClick?.(match),
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "bp-match-card-inner", children: match.participants.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.default.Fragment, { children: renderParticipant(p) }, p.id || i)) }),
|
|
82
|
+
!isCompressed && match.startTime && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", top: "-18px", left: "0", right: "0", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "bp-time-tag", children: match.startTime }) }),
|
|
83
|
+
!isCompressed && match.bestOf && match.bestOf > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", justifyContent: "center", gap: "4px", marginTop: "6px" }, children: Array.from({ length: match.bestOf }).map((_, i) => {
|
|
84
|
+
const totalPlayed = (match.participants[0].score || 0) + (match.participants[1].score || 0);
|
|
85
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
style: {
|
|
89
|
+
width: "4px",
|
|
90
|
+
height: "4px",
|
|
91
|
+
borderRadius: "50%",
|
|
92
|
+
backgroundColor: i < totalPlayed ? "var(--bp-accent)" : "var(--bp-border)"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
i
|
|
96
|
+
);
|
|
97
|
+
}) })
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// components/MatchDetailModal.tsx
|
|
104
|
+
var import_react2 = require("react");
|
|
105
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
106
|
+
var MatchDetailModal = ({ match, isOpen, onClose, onSave }) => {
|
|
107
|
+
const [localMatch, setLocalMatch] = (0, import_react2.useState)(() => JSON.parse(JSON.stringify(match)));
|
|
108
|
+
(0, import_react2.useEffect)(() => {
|
|
109
|
+
if (isOpen) {
|
|
110
|
+
setLocalMatch(JSON.parse(JSON.stringify(match)));
|
|
111
|
+
}
|
|
112
|
+
}, [isOpen, match]);
|
|
113
|
+
if (!isOpen) return null;
|
|
114
|
+
const updateScore = (gameIndex, participantIndex, val) => {
|
|
115
|
+
const score = parseInt(val) || 0;
|
|
116
|
+
const newMatch = { ...localMatch };
|
|
117
|
+
if (!newMatch.games || newMatch.games.length < (newMatch.bestOf || 1)) {
|
|
118
|
+
newMatch.games = Array(newMatch.bestOf || 1).fill({ score1: 0, score2: 0 });
|
|
119
|
+
}
|
|
120
|
+
const newGames = [...newMatch.games];
|
|
121
|
+
newGames[gameIndex] = { ...newGames[gameIndex], [participantIndex === 0 ? "score1" : "score2"]: score };
|
|
122
|
+
newMatch.games = newGames;
|
|
123
|
+
let s1 = 0, s2 = 0;
|
|
124
|
+
newGames.forEach((g) => {
|
|
125
|
+
if (g.score1 > g.score2) s1++;
|
|
126
|
+
else if (g.score2 > g.score1) s2++;
|
|
127
|
+
});
|
|
128
|
+
newMatch.participants[0].score = s1;
|
|
129
|
+
newMatch.participants[1].score = s2;
|
|
130
|
+
const threshold = Math.ceil((newMatch.bestOf || 1) / 2);
|
|
131
|
+
if (s1 >= threshold) newMatch.participants[0].status = "winner";
|
|
132
|
+
else if (s2 >= threshold) newMatch.participants[1].status = "winner";
|
|
133
|
+
else {
|
|
134
|
+
newMatch.participants[0].status = void 0;
|
|
135
|
+
newMatch.participants[1].status = void 0;
|
|
136
|
+
}
|
|
137
|
+
setLocalMatch(newMatch);
|
|
138
|
+
};
|
|
139
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "bp-modal-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bp-modal-content", children: [
|
|
140
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bp-modal-header", children: [
|
|
141
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
142
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "bp-section-title", children: "Match Report" }),
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "bp-round-title", children: [
|
|
144
|
+
"Series (Best of ",
|
|
145
|
+
localMatch.bestOf || 1,
|
|
146
|
+
")"
|
|
147
|
+
] })
|
|
148
|
+
] }),
|
|
149
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: onClose, style: { background: "none", border: "none", color: "white", cursor: "pointer" }, children: "\xD7" })
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bp-modal-body", children: [
|
|
152
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", textAlign: "center", gap: "20px" }, children: [
|
|
153
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { flex: 1 }, children: [
|
|
154
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 900, marginBottom: "8px", fontSize: "14px" }, children: localMatch.participants[0].name }),
|
|
155
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: "48px", fontWeight: 900 }, children: localMatch.participants[0].score || 0 })
|
|
156
|
+
] }),
|
|
157
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 900, opacity: 0.2 }, children: "VS" }),
|
|
158
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { flex: 1 }, children: [
|
|
159
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 900, marginBottom: "8px", fontSize: "14px" }, children: localMatch.participants[1].name }),
|
|
160
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: "48px", fontWeight: 900 }, children: localMatch.participants[1].score || 0 })
|
|
161
|
+
] })
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
|
|
164
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "bp-round-title", style: { marginBottom: "8px" }, children: "Individual Games" }),
|
|
165
|
+
Array.from({ length: localMatch.bestOf || 1 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "12px", alignItems: "center", background: "rgba(255,255,255,0.03)", padding: "12px", borderRadius: "12px" }, children: [
|
|
166
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { width: "40px", fontSize: "10px", fontWeight: 900, color: "var(--bp-text-muted)" }, children: [
|
|
167
|
+
"G",
|
|
168
|
+
i + 1
|
|
169
|
+
] }),
|
|
170
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
171
|
+
"input",
|
|
172
|
+
{
|
|
173
|
+
type: "number",
|
|
174
|
+
value: localMatch.games?.[i]?.score1 ?? "",
|
|
175
|
+
onChange: (e) => updateScore(i, 0, e.target.value),
|
|
176
|
+
style: { flex: 1, background: "#020617", border: "1px solid var(--bp-border)", color: "white", padding: "8px", borderRadius: "8px", textAlign: "center" }
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
180
|
+
"input",
|
|
181
|
+
{
|
|
182
|
+
type: "number",
|
|
183
|
+
value: localMatch.games?.[i]?.score2 ?? "",
|
|
184
|
+
onChange: (e) => updateScore(i, 1, e.target.value),
|
|
185
|
+
style: { flex: 1, background: "#020617", border: "1px solid var(--bp-border)", color: "white", padding: "8px", borderRadius: "8px", textAlign: "center" }
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] }, i))
|
|
189
|
+
] })
|
|
190
|
+
] }),
|
|
191
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bp-modal-footer", children: [
|
|
192
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "bp-btn bp-btn-secondary", onClick: onClose, children: "Cancel" }),
|
|
193
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "bp-btn bp-btn-primary", onClick: () => onSave(localMatch), children: "Save Result" })
|
|
194
|
+
] })
|
|
195
|
+
] }) });
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// components/TournamentBracket.tsx
|
|
199
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
200
|
+
var TournamentBracket = ({
|
|
201
|
+
data,
|
|
202
|
+
onUpdateMatch,
|
|
203
|
+
theme,
|
|
204
|
+
mode: controlledMode,
|
|
205
|
+
className = ""
|
|
206
|
+
}) => {
|
|
207
|
+
const [selectedMatch, setSelectedMatch] = (0, import_react3.useState)(null);
|
|
208
|
+
const [internalMode, setInternalMode] = (0, import_react3.useState)("detailed");
|
|
209
|
+
(0, import_react3.useEffect)(() => {
|
|
210
|
+
if (controlledMode) {
|
|
211
|
+
setInternalMode(controlledMode);
|
|
212
|
+
} else {
|
|
213
|
+
const handleResize = () => {
|
|
214
|
+
if (window.innerWidth < 768 && data.config?.autoMobileCompression !== false) {
|
|
215
|
+
setInternalMode("compressed");
|
|
216
|
+
} else {
|
|
217
|
+
setInternalMode("detailed");
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
handleResize();
|
|
221
|
+
window.addEventListener("resize", handleResize);
|
|
222
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
223
|
+
}
|
|
224
|
+
}, [controlledMode, data.config?.autoMobileCompression]);
|
|
225
|
+
const dynamicStyles = (0, import_react3.useMemo)(() => {
|
|
226
|
+
if (!theme) return {};
|
|
227
|
+
const styles = {};
|
|
228
|
+
if (theme.background) styles["--bp-bg"] = theme.background;
|
|
229
|
+
if (theme.cardBackground) styles["--bp-card-bg"] = theme.cardBackground;
|
|
230
|
+
if (theme.accentColor) styles["--bp-accent"] = theme.accentColor;
|
|
231
|
+
if (theme.textColor) styles["--bp-text"] = theme.textColor;
|
|
232
|
+
if (theme.textColorMuted) styles["--bp-text-muted"] = theme.textColorMuted;
|
|
233
|
+
if (theme.borderColor) styles["--bp-border"] = theme.borderColor;
|
|
234
|
+
if (theme.winnerBackground) styles["--bp-winner-bg"] = theme.winnerBackground;
|
|
235
|
+
if (theme.winnerTextColor) styles["--bp-winner-text"] = theme.winnerTextColor;
|
|
236
|
+
if (theme.cardWidth) styles["--bp-card-width"] = typeof theme.cardWidth === "number" ? `${theme.cardWidth}px` : theme.cardWidth;
|
|
237
|
+
if (theme.roundGap) styles["--bp-round-gap"] = typeof theme.roundGap === "number" ? `${theme.roundGap}px` : theme.roundGap;
|
|
238
|
+
if (theme.matchGap) styles["--bp-match-gap"] = typeof theme.matchGap === "number" ? `${theme.matchGap}px` : theme.matchGap;
|
|
239
|
+
if (theme.borderRadius) styles["--bp-border-radius"] = typeof theme.borderRadius === "number" ? `${theme.borderRadius}px` : theme.borderRadius;
|
|
240
|
+
if (theme.fontSize) styles["--bp-font-size"] = typeof theme.fontSize === "number" ? `${theme.fontSize}px` : theme.fontSize;
|
|
241
|
+
if (theme.fontFamily) styles["--bp-font-family"] = theme.fontFamily;
|
|
242
|
+
return styles;
|
|
243
|
+
}, [theme]);
|
|
244
|
+
const handleMatchUpdate = (updatedMatch) => {
|
|
245
|
+
onUpdateMatch?.(updatedMatch);
|
|
246
|
+
setSelectedMatch(null);
|
|
247
|
+
};
|
|
248
|
+
const renderRounds = (rounds) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bp-rounds-wrapper", children: rounds.map((round) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "bp-round-column", children: [
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bp-round-title-box", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "bp-round-title", children: round.title }) }),
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bp-matches-list", children: round.matches.map((match) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
251
|
+
BracketMatch,
|
|
252
|
+
{
|
|
253
|
+
match,
|
|
254
|
+
onMatchClick: setSelectedMatch,
|
|
255
|
+
mode: internalMode
|
|
256
|
+
},
|
|
257
|
+
match.id
|
|
258
|
+
)) })
|
|
259
|
+
] }, round.id)) });
|
|
260
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
261
|
+
"div",
|
|
262
|
+
{
|
|
263
|
+
className: `bp-container ${className}`,
|
|
264
|
+
style: dynamicStyles,
|
|
265
|
+
children: [
|
|
266
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "bp-section", children: [
|
|
267
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "bp-section-header", children: [
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "bp-section-title", children: "Winners Bracket" }),
|
|
269
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bp-section-line" })
|
|
270
|
+
] }),
|
|
271
|
+
renderRounds(data.rounds)
|
|
272
|
+
] }),
|
|
273
|
+
data.type === "double" && data.losersRounds && !data.config?.noComeback && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "bp-section", children: [
|
|
274
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "bp-section-header", children: [
|
|
275
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "bp-section-title", children: "Losers Bracket" }),
|
|
276
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bp-section-line" })
|
|
277
|
+
] }),
|
|
278
|
+
renderRounds(data.losersRounds)
|
|
279
|
+
] }),
|
|
280
|
+
(data.grandFinal || data.config?.noComeback && data.rounds[data.rounds.length - 1].matches[0]) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "bp-section", style: { alignItems: "center", borderTop: "1px solid var(--bp-border)", paddingTop: "60px" }, children: [
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { textAlign: "center", marginBottom: "20px" }, children: [
|
|
282
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "bp-section-title", children: "Finals" }),
|
|
283
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "bp-round-title", children: data.config?.noSecondaryFinal ? "Championship Match" : "Grand Final" })
|
|
284
|
+
] }),
|
|
285
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "var(--bp-round-gap)" }, children: [
|
|
286
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
287
|
+
BracketMatch,
|
|
288
|
+
{
|
|
289
|
+
match: data.grandFinal || data.rounds[data.rounds.length - 1].matches[0],
|
|
290
|
+
onMatchClick: setSelectedMatch,
|
|
291
|
+
mode: "detailed"
|
|
292
|
+
}
|
|
293
|
+
),
|
|
294
|
+
data.type === "double" && !data.config?.noSecondaryFinal && data.grandFinalReset && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
295
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: "var(--bp-round-gap)", height: "1px", backgroundColor: "var(--bp-border)" } }),
|
|
296
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
297
|
+
BracketMatch,
|
|
298
|
+
{
|
|
299
|
+
match: data.grandFinalReset,
|
|
300
|
+
onMatchClick: setSelectedMatch,
|
|
301
|
+
mode: "detailed"
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
] })
|
|
305
|
+
] })
|
|
306
|
+
] }),
|
|
307
|
+
selectedMatch && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
308
|
+
MatchDetailModal,
|
|
309
|
+
{
|
|
310
|
+
match: selectedMatch,
|
|
311
|
+
isOpen: !!selectedMatch,
|
|
312
|
+
onClose: () => setSelectedMatch(null),
|
|
313
|
+
onSave: handleMatchUpdate
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
321
|
+
0 && (module.exports = {
|
|
322
|
+
BracketMatch,
|
|
323
|
+
MatchDetailModal,
|
|
324
|
+
TournamentBracket
|
|
325
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// components/TournamentBracket.tsx
|
|
2
|
+
import { useState as useState2, useEffect as useEffect2, useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
// components/BracketMatch.tsx
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var BracketMatch = ({
|
|
8
|
+
match,
|
|
9
|
+
onMatchClick,
|
|
10
|
+
mode = "detailed"
|
|
11
|
+
}) => {
|
|
12
|
+
const isCompressed = mode === "compressed";
|
|
13
|
+
const renderParticipant = (p) => {
|
|
14
|
+
const isWinner = p.status === "winner";
|
|
15
|
+
const isTBD = !p.name || p.name === "TBD" || p.name.includes("---");
|
|
16
|
+
return /* @__PURE__ */ jsxs(
|
|
17
|
+
"div",
|
|
18
|
+
{
|
|
19
|
+
className: `bp-participant ${isWinner ? "is-winner" : ""} ${isTBD ? "is-tbd" : ""}`,
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", overflow: "hidden" }, children: [
|
|
22
|
+
!isCompressed && p.picture && /* @__PURE__ */ jsx(
|
|
23
|
+
"img",
|
|
24
|
+
{
|
|
25
|
+
src: p.picture,
|
|
26
|
+
style: { width: "20px", height: "20px", borderRadius: "50%", objectFit: "cover" },
|
|
27
|
+
alt: ""
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
/* @__PURE__ */ jsx("span", { className: "bp-team-name", children: isCompressed ? p.code || p.name.substring(0, 3) : p.name || "TBD" })
|
|
31
|
+
] }),
|
|
32
|
+
/* @__PURE__ */ jsx("span", { className: "bp-participant-score", children: p.score ?? "-" })
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
return /* @__PURE__ */ jsxs(
|
|
38
|
+
"div",
|
|
39
|
+
{
|
|
40
|
+
className: `bp-match-card ${isCompressed ? "is-compressed" : ""}`,
|
|
41
|
+
onClick: () => onMatchClick?.(match),
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsx("div", { className: "bp-match-card-inner", children: match.participants.map((p, i) => /* @__PURE__ */ jsx(React.Fragment, { children: renderParticipant(p) }, p.id || i)) }),
|
|
44
|
+
!isCompressed && match.startTime && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: "-18px", left: "0", right: "0", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("span", { className: "bp-time-tag", children: match.startTime }) }),
|
|
45
|
+
!isCompressed && match.bestOf && match.bestOf > 1 && /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", gap: "4px", marginTop: "6px" }, children: Array.from({ length: match.bestOf }).map((_, i) => {
|
|
46
|
+
const totalPlayed = (match.participants[0].score || 0) + (match.participants[1].score || 0);
|
|
47
|
+
return /* @__PURE__ */ jsx(
|
|
48
|
+
"div",
|
|
49
|
+
{
|
|
50
|
+
style: {
|
|
51
|
+
width: "4px",
|
|
52
|
+
height: "4px",
|
|
53
|
+
borderRadius: "50%",
|
|
54
|
+
backgroundColor: i < totalPlayed ? "var(--bp-accent)" : "var(--bp-border)"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
i
|
|
58
|
+
);
|
|
59
|
+
}) })
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// components/MatchDetailModal.tsx
|
|
66
|
+
import { useState, useEffect } from "react";
|
|
67
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
68
|
+
var MatchDetailModal = ({ match, isOpen, onClose, onSave }) => {
|
|
69
|
+
const [localMatch, setLocalMatch] = useState(() => JSON.parse(JSON.stringify(match)));
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (isOpen) {
|
|
72
|
+
setLocalMatch(JSON.parse(JSON.stringify(match)));
|
|
73
|
+
}
|
|
74
|
+
}, [isOpen, match]);
|
|
75
|
+
if (!isOpen) return null;
|
|
76
|
+
const updateScore = (gameIndex, participantIndex, val) => {
|
|
77
|
+
const score = parseInt(val) || 0;
|
|
78
|
+
const newMatch = { ...localMatch };
|
|
79
|
+
if (!newMatch.games || newMatch.games.length < (newMatch.bestOf || 1)) {
|
|
80
|
+
newMatch.games = Array(newMatch.bestOf || 1).fill({ score1: 0, score2: 0 });
|
|
81
|
+
}
|
|
82
|
+
const newGames = [...newMatch.games];
|
|
83
|
+
newGames[gameIndex] = { ...newGames[gameIndex], [participantIndex === 0 ? "score1" : "score2"]: score };
|
|
84
|
+
newMatch.games = newGames;
|
|
85
|
+
let s1 = 0, s2 = 0;
|
|
86
|
+
newGames.forEach((g) => {
|
|
87
|
+
if (g.score1 > g.score2) s1++;
|
|
88
|
+
else if (g.score2 > g.score1) s2++;
|
|
89
|
+
});
|
|
90
|
+
newMatch.participants[0].score = s1;
|
|
91
|
+
newMatch.participants[1].score = s2;
|
|
92
|
+
const threshold = Math.ceil((newMatch.bestOf || 1) / 2);
|
|
93
|
+
if (s1 >= threshold) newMatch.participants[0].status = "winner";
|
|
94
|
+
else if (s2 >= threshold) newMatch.participants[1].status = "winner";
|
|
95
|
+
else {
|
|
96
|
+
newMatch.participants[0].status = void 0;
|
|
97
|
+
newMatch.participants[1].status = void 0;
|
|
98
|
+
}
|
|
99
|
+
setLocalMatch(newMatch);
|
|
100
|
+
};
|
|
101
|
+
return /* @__PURE__ */ jsx2("div", { className: "bp-modal-overlay", children: /* @__PURE__ */ jsxs2("div", { className: "bp-modal-content", children: [
|
|
102
|
+
/* @__PURE__ */ jsxs2("div", { className: "bp-modal-header", children: [
|
|
103
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
104
|
+
/* @__PURE__ */ jsx2("h2", { className: "bp-section-title", children: "Match Report" }),
|
|
105
|
+
/* @__PURE__ */ jsxs2("span", { className: "bp-round-title", children: [
|
|
106
|
+
"Series (Best of ",
|
|
107
|
+
localMatch.bestOf || 1,
|
|
108
|
+
")"
|
|
109
|
+
] })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsx2("button", { onClick: onClose, style: { background: "none", border: "none", color: "white", cursor: "pointer" }, children: "\xD7" })
|
|
112
|
+
] }),
|
|
113
|
+
/* @__PURE__ */ jsxs2("div", { className: "bp-modal-body", children: [
|
|
114
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", textAlign: "center", gap: "20px" }, children: [
|
|
115
|
+
/* @__PURE__ */ jsxs2("div", { style: { flex: 1 }, children: [
|
|
116
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 900, marginBottom: "8px", fontSize: "14px" }, children: localMatch.participants[0].name }),
|
|
117
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: "48px", fontWeight: 900 }, children: localMatch.participants[0].score || 0 })
|
|
118
|
+
] }),
|
|
119
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 900, opacity: 0.2 }, children: "VS" }),
|
|
120
|
+
/* @__PURE__ */ jsxs2("div", { style: { flex: 1 }, children: [
|
|
121
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 900, marginBottom: "8px", fontSize: "14px" }, children: localMatch.participants[1].name }),
|
|
122
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: "48px", fontWeight: 900 }, children: localMatch.participants[1].score || 0 })
|
|
123
|
+
] })
|
|
124
|
+
] }),
|
|
125
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
|
|
126
|
+
/* @__PURE__ */ jsx2("div", { className: "bp-round-title", style: { marginBottom: "8px" }, children: "Individual Games" }),
|
|
127
|
+
Array.from({ length: localMatch.bestOf || 1 }).map((_, i) => /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "12px", alignItems: "center", background: "rgba(255,255,255,0.03)", padding: "12px", borderRadius: "12px" }, children: [
|
|
128
|
+
/* @__PURE__ */ jsxs2("div", { style: { width: "40px", fontSize: "10px", fontWeight: 900, color: "var(--bp-text-muted)" }, children: [
|
|
129
|
+
"G",
|
|
130
|
+
i + 1
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsx2(
|
|
133
|
+
"input",
|
|
134
|
+
{
|
|
135
|
+
type: "number",
|
|
136
|
+
value: localMatch.games?.[i]?.score1 ?? "",
|
|
137
|
+
onChange: (e) => updateScore(i, 0, e.target.value),
|
|
138
|
+
style: { flex: 1, background: "#020617", border: "1px solid var(--bp-border)", color: "white", padding: "8px", borderRadius: "8px", textAlign: "center" }
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsx2(
|
|
142
|
+
"input",
|
|
143
|
+
{
|
|
144
|
+
type: "number",
|
|
145
|
+
value: localMatch.games?.[i]?.score2 ?? "",
|
|
146
|
+
onChange: (e) => updateScore(i, 1, e.target.value),
|
|
147
|
+
style: { flex: 1, background: "#020617", border: "1px solid var(--bp-border)", color: "white", padding: "8px", borderRadius: "8px", textAlign: "center" }
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
] }, i))
|
|
151
|
+
] })
|
|
152
|
+
] }),
|
|
153
|
+
/* @__PURE__ */ jsxs2("div", { className: "bp-modal-footer", children: [
|
|
154
|
+
/* @__PURE__ */ jsx2("button", { className: "bp-btn bp-btn-secondary", onClick: onClose, children: "Cancel" }),
|
|
155
|
+
/* @__PURE__ */ jsx2("button", { className: "bp-btn bp-btn-primary", onClick: () => onSave(localMatch), children: "Save Result" })
|
|
156
|
+
] })
|
|
157
|
+
] }) });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// components/TournamentBracket.tsx
|
|
161
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
162
|
+
var TournamentBracket = ({
|
|
163
|
+
data,
|
|
164
|
+
onUpdateMatch,
|
|
165
|
+
theme,
|
|
166
|
+
mode: controlledMode,
|
|
167
|
+
className = ""
|
|
168
|
+
}) => {
|
|
169
|
+
const [selectedMatch, setSelectedMatch] = useState2(null);
|
|
170
|
+
const [internalMode, setInternalMode] = useState2("detailed");
|
|
171
|
+
useEffect2(() => {
|
|
172
|
+
if (controlledMode) {
|
|
173
|
+
setInternalMode(controlledMode);
|
|
174
|
+
} else {
|
|
175
|
+
const handleResize = () => {
|
|
176
|
+
if (window.innerWidth < 768 && data.config?.autoMobileCompression !== false) {
|
|
177
|
+
setInternalMode("compressed");
|
|
178
|
+
} else {
|
|
179
|
+
setInternalMode("detailed");
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
handleResize();
|
|
183
|
+
window.addEventListener("resize", handleResize);
|
|
184
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
185
|
+
}
|
|
186
|
+
}, [controlledMode, data.config?.autoMobileCompression]);
|
|
187
|
+
const dynamicStyles = useMemo(() => {
|
|
188
|
+
if (!theme) return {};
|
|
189
|
+
const styles = {};
|
|
190
|
+
if (theme.background) styles["--bp-bg"] = theme.background;
|
|
191
|
+
if (theme.cardBackground) styles["--bp-card-bg"] = theme.cardBackground;
|
|
192
|
+
if (theme.accentColor) styles["--bp-accent"] = theme.accentColor;
|
|
193
|
+
if (theme.textColor) styles["--bp-text"] = theme.textColor;
|
|
194
|
+
if (theme.textColorMuted) styles["--bp-text-muted"] = theme.textColorMuted;
|
|
195
|
+
if (theme.borderColor) styles["--bp-border"] = theme.borderColor;
|
|
196
|
+
if (theme.winnerBackground) styles["--bp-winner-bg"] = theme.winnerBackground;
|
|
197
|
+
if (theme.winnerTextColor) styles["--bp-winner-text"] = theme.winnerTextColor;
|
|
198
|
+
if (theme.cardWidth) styles["--bp-card-width"] = typeof theme.cardWidth === "number" ? `${theme.cardWidth}px` : theme.cardWidth;
|
|
199
|
+
if (theme.roundGap) styles["--bp-round-gap"] = typeof theme.roundGap === "number" ? `${theme.roundGap}px` : theme.roundGap;
|
|
200
|
+
if (theme.matchGap) styles["--bp-match-gap"] = typeof theme.matchGap === "number" ? `${theme.matchGap}px` : theme.matchGap;
|
|
201
|
+
if (theme.borderRadius) styles["--bp-border-radius"] = typeof theme.borderRadius === "number" ? `${theme.borderRadius}px` : theme.borderRadius;
|
|
202
|
+
if (theme.fontSize) styles["--bp-font-size"] = typeof theme.fontSize === "number" ? `${theme.fontSize}px` : theme.fontSize;
|
|
203
|
+
if (theme.fontFamily) styles["--bp-font-family"] = theme.fontFamily;
|
|
204
|
+
return styles;
|
|
205
|
+
}, [theme]);
|
|
206
|
+
const handleMatchUpdate = (updatedMatch) => {
|
|
207
|
+
onUpdateMatch?.(updatedMatch);
|
|
208
|
+
setSelectedMatch(null);
|
|
209
|
+
};
|
|
210
|
+
const renderRounds = (rounds) => /* @__PURE__ */ jsx3("div", { className: "bp-rounds-wrapper", children: rounds.map((round) => /* @__PURE__ */ jsxs3("div", { className: "bp-round-column", children: [
|
|
211
|
+
/* @__PURE__ */ jsx3("div", { className: "bp-round-title-box", children: /* @__PURE__ */ jsx3("h3", { className: "bp-round-title", children: round.title }) }),
|
|
212
|
+
/* @__PURE__ */ jsx3("div", { className: "bp-matches-list", children: round.matches.map((match) => /* @__PURE__ */ jsx3(
|
|
213
|
+
BracketMatch,
|
|
214
|
+
{
|
|
215
|
+
match,
|
|
216
|
+
onMatchClick: setSelectedMatch,
|
|
217
|
+
mode: internalMode
|
|
218
|
+
},
|
|
219
|
+
match.id
|
|
220
|
+
)) })
|
|
221
|
+
] }, round.id)) });
|
|
222
|
+
return /* @__PURE__ */ jsxs3(
|
|
223
|
+
"div",
|
|
224
|
+
{
|
|
225
|
+
className: `bp-container ${className}`,
|
|
226
|
+
style: dynamicStyles,
|
|
227
|
+
children: [
|
|
228
|
+
/* @__PURE__ */ jsxs3("section", { className: "bp-section", children: [
|
|
229
|
+
/* @__PURE__ */ jsxs3("div", { className: "bp-section-header", children: [
|
|
230
|
+
/* @__PURE__ */ jsx3("h2", { className: "bp-section-title", children: "Winners Bracket" }),
|
|
231
|
+
/* @__PURE__ */ jsx3("div", { className: "bp-section-line" })
|
|
232
|
+
] }),
|
|
233
|
+
renderRounds(data.rounds)
|
|
234
|
+
] }),
|
|
235
|
+
data.type === "double" && data.losersRounds && !data.config?.noComeback && /* @__PURE__ */ jsxs3("section", { className: "bp-section", children: [
|
|
236
|
+
/* @__PURE__ */ jsxs3("div", { className: "bp-section-header", children: [
|
|
237
|
+
/* @__PURE__ */ jsx3("h2", { className: "bp-section-title", children: "Losers Bracket" }),
|
|
238
|
+
/* @__PURE__ */ jsx3("div", { className: "bp-section-line" })
|
|
239
|
+
] }),
|
|
240
|
+
renderRounds(data.losersRounds)
|
|
241
|
+
] }),
|
|
242
|
+
(data.grandFinal || data.config?.noComeback && data.rounds[data.rounds.length - 1].matches[0]) && /* @__PURE__ */ jsxs3("section", { className: "bp-section", style: { alignItems: "center", borderTop: "1px solid var(--bp-border)", paddingTop: "60px" }, children: [
|
|
243
|
+
/* @__PURE__ */ jsxs3("div", { style: { textAlign: "center", marginBottom: "20px" }, children: [
|
|
244
|
+
/* @__PURE__ */ jsx3("h2", { className: "bp-section-title", children: "Finals" }),
|
|
245
|
+
/* @__PURE__ */ jsx3("p", { className: "bp-round-title", children: data.config?.noSecondaryFinal ? "Championship Match" : "Grand Final" })
|
|
246
|
+
] }),
|
|
247
|
+
/* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: "var(--bp-round-gap)" }, children: [
|
|
248
|
+
/* @__PURE__ */ jsx3(
|
|
249
|
+
BracketMatch,
|
|
250
|
+
{
|
|
251
|
+
match: data.grandFinal || data.rounds[data.rounds.length - 1].matches[0],
|
|
252
|
+
onMatchClick: setSelectedMatch,
|
|
253
|
+
mode: "detailed"
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
data.type === "double" && !data.config?.noSecondaryFinal && data.grandFinalReset && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
257
|
+
/* @__PURE__ */ jsx3("div", { style: { width: "var(--bp-round-gap)", height: "1px", backgroundColor: "var(--bp-border)" } }),
|
|
258
|
+
/* @__PURE__ */ jsx3(
|
|
259
|
+
BracketMatch,
|
|
260
|
+
{
|
|
261
|
+
match: data.grandFinalReset,
|
|
262
|
+
onMatchClick: setSelectedMatch,
|
|
263
|
+
mode: "detailed"
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
] })
|
|
267
|
+
] })
|
|
268
|
+
] }),
|
|
269
|
+
selectedMatch && /* @__PURE__ */ jsx3(
|
|
270
|
+
MatchDetailModal,
|
|
271
|
+
{
|
|
272
|
+
match: selectedMatch,
|
|
273
|
+
isOpen: !!selectedMatch,
|
|
274
|
+
onClose: () => setSelectedMatch(null),
|
|
275
|
+
onSave: handleMatchUpdate
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
export {
|
|
283
|
+
BracketMatch,
|
|
284
|
+
MatchDetailModal,
|
|
285
|
+
TournamentBracket
|
|
286
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
{
|
|
3
|
+
"name": "bracket-pro-react",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A high-performance, framework-agnostic tournament bracket component for React and Next.js. Supports Single and Double Elimination with deep design customization.",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bracket-styles.css"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "next dev",
|
|
15
|
+
"build": "tsup index.tsx --format cjs,esm --dts --clean",
|
|
16
|
+
"lint": "eslint ."
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react",
|
|
20
|
+
"bracket",
|
|
21
|
+
"tournament",
|
|
22
|
+
"e-sports",
|
|
23
|
+
"double-elimination",
|
|
24
|
+
"single-elimination",
|
|
25
|
+
"nextjs",
|
|
26
|
+
"typescript"
|
|
27
|
+
],
|
|
28
|
+
"author": "Your Name / Organization",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=18.0.0",
|
|
32
|
+
"react-dom": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.0.0",
|
|
36
|
+
"@types/react-dom": "^19.0.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|