@amirhossein-shk/tournament-bracket-js 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 ADDED
@@ -0,0 +1,821 @@
1
+ # Tournament Bracket
2
+
3
+ A lightweight JavaScript library for rendering and managing single-elimination tournament brackets in the browser.
4
+
5
+ It supports both classic browser usage with a `<script>` tag and modern ESM usage with `import`. The library renders bracket rounds, matches, players, scores, avatars, and SVG connector lines, while also exposing a small API for updating matches, finishing matches, reading state, and handling events.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - Render single-elimination tournament brackets
12
+ - Use with a normal browser `<script>` tag
13
+ - Use with modern ESM `import`
14
+ - Support player names, scores, avatars, and fallback avatars
15
+ - Automatically normalize missing player data
16
+ - Validate bracket structure before rendering
17
+ - Update match data after initialization
18
+ - Set individual player scores
19
+ - Finish matches and propagate winners
20
+ - Listen to match click, update, and finish events
21
+ - Ship separate JavaScript and CSS builds
22
+ - Include both minified and non-minified output files
23
+
24
+ ---
25
+
26
+ ## Demo
27
+
28
+ Try the live demo:
29
+
30
+ [https://amirhossein-shk.github.io/tournament-bracket/demo/](https://amirhossein-shk.github.io/tournament-bracket/demo/)
31
+
32
+ ---
33
+
34
+ ## File Structure
35
+
36
+ Recommended project structure:
37
+
38
+ ```txt
39
+ project/
40
+ src/
41
+ tournament-bracket.js
42
+ tournament-bracket.scss
43
+ entry-browser.js
44
+
45
+ dist/
46
+ tournament-bracket.js
47
+ tournament-bracket.min.js
48
+ tournament-bracket.esm.js
49
+ tournament-bracket.esm.min.js
50
+ tournament-bracket.css
51
+ tournament-bracket.min.css
52
+
53
+ demo/
54
+ index.html
55
+ demo.js
56
+ demo.css
57
+
58
+ README.md
59
+ package.json
60
+ vite.config.js
61
+ ```
62
+
63
+ ### Folder Purpose
64
+
65
+ | Folder | Purpose |
66
+ |---|---|
67
+ | `src/` | Source code of the library |
68
+ | `dist/` | Final build output for users |
69
+ | `demo/` | Live demo and usage reference |
70
+
71
+ ---
72
+
73
+ ## Build Files
74
+
75
+ The `dist` folder contains the final files that users should consume.
76
+
77
+ ```txt
78
+ dist/
79
+ tournament-bracket.js
80
+ tournament-bracket.min.js
81
+ tournament-bracket.esm.js
82
+ tournament-bracket.esm.min.js
83
+ tournament-bracket.css
84
+ tournament-bracket.min.css
85
+ ```
86
+
87
+ ### JavaScript Files
88
+
89
+ | File | Usage |
90
+ |---|---|
91
+ | `tournament-bracket.js` | Browser build, non-minified |
92
+ | `tournament-bracket.min.js` | Browser build, minified |
93
+ | `tournament-bracket.esm.js` | ESM build, non-minified |
94
+ | `tournament-bracket.esm.min.js` | ESM build, minified |
95
+
96
+ ### CSS Files
97
+
98
+ | File | Usage |
99
+ |---|---|
100
+ | `tournament-bracket.css` | Stylesheet, non-minified |
101
+ | `tournament-bracket.min.css` | Stylesheet, minified |
102
+
103
+ The CSS is intentionally shipped separately from JavaScript. This makes styling easier to override, easier to debug, and easier to use with bundlers or CDNs.
104
+
105
+ ---
106
+
107
+ ## Browser Usage
108
+
109
+ Use this option when you want to include the library directly in an HTML page.
110
+
111
+ ```html
112
+ <link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
113
+ <script src="./dist/tournament-bracket.min.js"></script>
114
+ ```
115
+
116
+ After loading the browser build, the global function is available as:
117
+
118
+ ```js
119
+ tournamentBracket(...)
120
+ ```
121
+
122
+ ### Complete Browser Example
123
+
124
+ ```html
125
+ <!doctype html>
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="UTF-8" />
129
+ <title>Tournament Bracket</title>
130
+ <link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
131
+ </head>
132
+ <body>
133
+ <div id="tournament-bracket"></div>
134
+
135
+ <script src="./dist/tournament-bracket.min.js"></script>
136
+ <script>
137
+ const tb = tournamentBracket({
138
+ targetId: "tournament-bracket",
139
+ rounds: [
140
+ {
141
+ roundId: "r1",
142
+ name: "Quarter Finals",
143
+ matches: [
144
+ {
145
+ matchId: "m1",
146
+ isFinished: true,
147
+ players: [
148
+ { name: "Player 1", score: 2, avatarUrl: "" },
149
+ { name: "Player 2", score: 1, avatarUrl: "" }
150
+ ]
151
+ },
152
+ {
153
+ matchId: "m2",
154
+ players: [
155
+ { name: "Player 3", score: "-" },
156
+ { name: "Player 4", score: "-" }
157
+ ]
158
+ }
159
+ ]
160
+ },
161
+ {
162
+ roundId: "r2",
163
+ name: "Semi Finals",
164
+ matches: [
165
+ {
166
+ matchId: "m3",
167
+ players: [{}, {}]
168
+ }
169
+ ]
170
+ }
171
+ ]
172
+ });
173
+
174
+ tb.init();
175
+ </script>
176
+ </body>
177
+ </html>
178
+ ```
179
+
180
+ ---
181
+
182
+ ## ESM Usage
183
+
184
+ Use this option when working with a module-based setup.
185
+
186
+ ```js
187
+ import tournamentBracket from "./dist/tournament-bracket.esm.js";
188
+ import "./dist/tournament-bracket.css";
189
+ ```
190
+
191
+ ### Complete ESM Example
192
+
193
+ ```js
194
+ import tournamentBracket from "./dist/tournament-bracket.esm.js";
195
+ import "./dist/tournament-bracket.css";
196
+
197
+ const tb = tournamentBracket({
198
+ targetId: "tournament-bracket",
199
+ rounds: [
200
+ {
201
+ roundId: "r1",
202
+ name: "Quarter Finals",
203
+ matches: [
204
+ {
205
+ matchId: "m1",
206
+ isFinished: true,
207
+ players: [
208
+ { name: "Player 1", score: 2, avatarUrl: "" },
209
+ { name: "Player 2", score: 1, avatarUrl: "" }
210
+ ]
211
+ },
212
+ {
213
+ matchId: "m2",
214
+ players: [
215
+ { name: "Player 3", score: "-" },
216
+ { name: "Player 4", score: "-" }
217
+ ]
218
+ }
219
+ ]
220
+ },
221
+ {
222
+ roundId: "r2",
223
+ name: "Semi Finals",
224
+ matches: [
225
+ {
226
+ matchId: "m3",
227
+ players: [{}, {}]
228
+ }
229
+ ]
230
+ }
231
+ ]
232
+ });
233
+
234
+ tb.init();
235
+ ```
236
+
237
+ If you use ESM directly in the browser, load your code with:
238
+
239
+ ```html
240
+ <script type="module" src="./your-file.js"></script>
241
+ ```
242
+
243
+ Do not open ESM files directly with `file://`. Use a local development server instead.
244
+
245
+ ---
246
+
247
+ ## Required HTML
248
+
249
+ The page must contain a target container element:
250
+
251
+ ```html
252
+ <div id="tournament-bracket"></div>
253
+ ```
254
+
255
+ The `targetId` option must match this element ID:
256
+
257
+ ```js
258
+ const tb = tournamentBracket({
259
+ targetId: "tournament-bracket",
260
+ rounds: []
261
+ });
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Configuration
267
+
268
+ Create a bracket instance by calling:
269
+
270
+ ```js
271
+ const tb = tournamentBracket(config);
272
+ ```
273
+
274
+ ### Available Options
275
+
276
+ | Option | Type | Default | Description |
277
+ |---|---|---:|---|
278
+ | `targetId` | `string` | `"tournament-bracket"` | ID of the container element |
279
+ | `rounds` | `Array` | required | Tournament rounds data |
280
+ | `distance` | `number` | `42` | Internal spacing used for layout and connectors |
281
+ | `width` | `number` | `196` | Width of each match card |
282
+ | `matchHeight` | `number` | `94` | Height of each match card |
283
+ | `roundGap` | `number` | `96` | Horizontal gap between rounds |
284
+ | `avatarFallbackUrl` | `string` | `"./images/avatar.png"` | Fallback avatar when a player avatar is missing |
285
+ | `connectorColor` | `string` | `"white"` | Color of SVG connector lines |
286
+ | `onMatchClick` | `function` | noop | Called when a match is clicked |
287
+ | `onMatchUpdate` | `function` | noop | Called when a match is updated |
288
+ | `onMatchFinish` | `function` | noop | Called when a match is finished |
289
+
290
+ ---
291
+
292
+ ## Full Configuration Example
293
+
294
+ ```js
295
+ const sampleConfig = {
296
+ targetId: "tournament-bracket",
297
+ distance: 80,
298
+ width: 196,
299
+ matchHeight: 72,
300
+ roundGap: 80,
301
+ connectorColor: "#64748b",
302
+ rounds: [
303
+ {
304
+ roundId: "r1",
305
+ name: "Quarter Finals",
306
+ matches: [
307
+ {
308
+ matchId: "m1",
309
+ isFinished: true,
310
+ players: [
311
+ {
312
+ name: "Player 1",
313
+ score: 2,
314
+ avatarUrl: ""
315
+ },
316
+ {
317
+ name: "Player 2",
318
+ score: 1,
319
+ avatarUrl: ""
320
+ }
321
+ ]
322
+ },
323
+ {
324
+ matchId: "m2",
325
+ players: [
326
+ {
327
+ name: "Player 3",
328
+ score: "-"
329
+ },
330
+ {
331
+ name: "Player 4",
332
+ score: "-"
333
+ }
334
+ ]
335
+ }
336
+ ]
337
+ },
338
+ {
339
+ roundId: "r2",
340
+ name: "Semi Finals",
341
+ matches: [
342
+ {
343
+ matchId: "m3",
344
+ players: [{}, {}]
345
+ }
346
+ ]
347
+ }
348
+ ]
349
+ };
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Data Structure
355
+
356
+ ### Rounds
357
+
358
+ The `rounds` array defines the complete tournament structure.
359
+
360
+ ```js
361
+ rounds: [
362
+ {
363
+ roundId: "r1",
364
+ name: "Quarter Finals",
365
+ matches: [
366
+ {
367
+ matchId: "m1",
368
+ players: [
369
+ { name: "Player 1", score: 2 },
370
+ { name: "Player 2", score: 1 }
371
+ ]
372
+ }
373
+ ]
374
+ }
375
+ ]
376
+ ```
377
+
378
+ ### Round Object
379
+
380
+ | Field | Type | Required | Description |
381
+ |---|---|---|---|
382
+ | `roundId` | `string` | no | Optional custom round ID |
383
+ | `name` | `string` | no | Optional round name |
384
+ | `matches` | `Array` | yes | List of matches in this round |
385
+
386
+ ### Match Object
387
+
388
+ | Field | Type | Required | Description |
389
+ |---|---|---|---|
390
+ | `matchId` | `string` | no | Optional custom match ID |
391
+ | `isFinished` | `boolean` | no | Marks the match as completed |
392
+ | `players` | `Array` | yes | Exactly two players |
393
+
394
+ ### Player Object
395
+
396
+ | Field | Type | Required | Description |
397
+ |---|---|---|---|
398
+ | `name` | `string` | no | Player display name |
399
+ | `score` | `number \| string` | no | Player score, or `"-"` for pending score |
400
+ | `avatarUrl` | `string` | no | Player avatar URL |
401
+
402
+ ---
403
+
404
+ ## Default Player Values
405
+
406
+ If a player is missing or partially defined, the library fills missing fields with default values.
407
+
408
+ ```js
409
+ {
410
+ name: "-",
411
+ avatarUrl: "",
412
+ score: "-"
413
+ }
414
+ ```
415
+
416
+ This is valid:
417
+
418
+ ```js
419
+ players: [{}, {}]
420
+ ```
421
+
422
+ This is also valid:
423
+
424
+ ```js
425
+ players: [
426
+ { name: "Player 1" },
427
+ {}
428
+ ]
429
+ ```
430
+
431
+ Empty player objects are useful for future rounds where participants are not known yet.
432
+
433
+ ---
434
+
435
+ ## Validation Rules
436
+
437
+ The library validates the tournament structure before rendering.
438
+
439
+ The rules are:
440
+
441
+ - `rounds` must be a non-empty array
442
+ - every round must be an object
443
+ - every round must contain a `matches` array
444
+ - every match must be an object
445
+ - every match must contain exactly `2` players
446
+ - the first round match count must be a power of two
447
+ - each next round must contain exactly half the matches of the previous round
448
+ - the final round must contain exactly `1` match
449
+
450
+ ### Valid Bracket Shape
451
+
452
+ ```txt
453
+ Round 1: 8 matches
454
+ Round 2: 4 matches
455
+ Round 3: 2 matches
456
+ Round 4: 1 match
457
+ ```
458
+
459
+ ### Invalid Bracket Shape
460
+
461
+ ```txt
462
+ Round 1: 3 matches
463
+ Round 2: 2 matches
464
+ Round 3: 1 match
465
+ ```
466
+
467
+ This is invalid because the first round has `3` matches, and `3` is not a power of two.
468
+
469
+ Tournament math is strict. Sadly, it does not accept emotional arguments.
470
+
471
+ ---
472
+
473
+ ## Instance API
474
+
475
+ `tournamentBracket(config)` returns an object with these methods:
476
+
477
+ ```js
478
+ {
479
+ init,
480
+ updateMatch,
481
+ setMatchScore,
482
+ finishMatch,
483
+ destroy,
484
+ getState,
485
+ onMatchClick,
486
+ onMatchUpdate,
487
+ onMatchFinish
488
+ }
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Methods
494
+
495
+ ### `init()`
496
+
497
+ Initializes and renders the bracket.
498
+
499
+ ```js
500
+ tb.init();
501
+ ```
502
+
503
+ Call this after creating the instance.
504
+
505
+ ---
506
+
507
+ ### `updateMatch(matchId, patch)`
508
+
509
+ Updates an existing match by ID.
510
+
511
+ ```js
512
+ tb.updateMatch("m2", {
513
+ players: [
514
+ { name: "Player 3", score: 1 },
515
+ { name: "Player 4", score: 2 }
516
+ ]
517
+ });
518
+ ```
519
+
520
+ Use this when you want to update match data after initialization.
521
+
522
+ ---
523
+
524
+ ### `setMatchScore(matchId, playerIndex, score)`
525
+
526
+ Updates the score of one player in a match.
527
+
528
+ ```js
529
+ tb.setMatchScore("m2", 0, 3);
530
+ tb.setMatchScore("m2", 1, 1);
531
+ ```
532
+
533
+ Arguments:
534
+
535
+ | Argument | Description |
536
+ |---|---|
537
+ | `matchId` | ID of the match |
538
+ | `playerIndex` | Player index, either `0` or `1` |
539
+ | `score` | New player score |
540
+
541
+ ---
542
+
543
+ ### `finishMatch(matchId)`
544
+
545
+ Marks a match as finished.
546
+
547
+ ```js
548
+ tb.finishMatch("m2");
549
+ ```
550
+
551
+ When a match is finished, the library can resolve the winner and move the winner forward according to the bracket structure.
552
+
553
+ ---
554
+
555
+ ### `destroy()`
556
+
557
+ Clears the rendered bracket from the target container.
558
+
559
+ ```js
560
+ tb.destroy();
561
+ ```
562
+
563
+ Use this when you want to remove the bracket or re-render from scratch.
564
+
565
+ ---
566
+
567
+ ### `getState()`
568
+
569
+ Returns the current internal bracket state.
570
+
571
+ ```js
572
+ const state = tb.getState();
573
+ console.log(state);
574
+ ```
575
+
576
+ This is useful for debugging, inspection, or syncing with external application state.
577
+
578
+ ---
579
+
580
+ ## Event Callbacks
581
+
582
+ Callbacks can be passed in the initial configuration.
583
+
584
+ ```js
585
+ const tb = tournamentBracket({
586
+ ...sampleConfig,
587
+ onMatchClick: function (match) {
588
+ console.log("clicked", match);
589
+ },
590
+ onMatchUpdate: function (match) {
591
+ console.log("updated", match);
592
+ },
593
+ onMatchFinish: function (match) {
594
+ console.log("finished", match);
595
+ }
596
+ });
597
+ ```
598
+
599
+ They can also be registered or replaced later using the instance API.
600
+
601
+ ---
602
+
603
+ ### `onMatchClick(fn)`
604
+
605
+ Registers a callback for match click events.
606
+
607
+ ```js
608
+ tb.onMatchClick(function (match) {
609
+ console.log("clicked", match);
610
+ });
611
+ ```
612
+
613
+ ---
614
+
615
+ ### `onMatchUpdate(fn)`
616
+
617
+ Registers a callback for match update events.
618
+
619
+ ```js
620
+ tb.onMatchUpdate(function (match) {
621
+ console.log("updated", match);
622
+ });
623
+ ```
624
+
625
+ ---
626
+
627
+ ### `onMatchFinish(fn)`
628
+
629
+ Registers a callback for match finish events.
630
+
631
+ ```js
632
+ tb.onMatchFinish(function (match) {
633
+ console.log("finished", match);
634
+ });
635
+ ```
636
+
637
+ ---
638
+
639
+ ## Complete Event Example
640
+
641
+ ```js
642
+ const tb = tournamentBracket({
643
+ ...sampleConfig,
644
+ onMatchClick: function (match) {
645
+ console.log("config.onMatchClick", {
646
+ matchId: match.matchId,
647
+ players: match.players
648
+ });
649
+ },
650
+ onMatchUpdate: function (match) {
651
+ console.log("config.onMatchUpdate", {
652
+ matchId: match.matchId,
653
+ players: match.players,
654
+ isFinished: match.isFinished
655
+ });
656
+ },
657
+ onMatchFinish: function (match) {
658
+ console.log("config.onMatchFinish", {
659
+ matchId: match.matchId,
660
+ players: match.players
661
+ });
662
+ }
663
+ });
664
+
665
+ tb.init();
666
+ ```
667
+
668
+ ---
669
+
670
+ ## Typical Usage Flow
671
+
672
+ A common usage flow looks like this:
673
+
674
+ ```js
675
+ const tb = tournamentBracket(sampleConfig);
676
+
677
+ tb.init();
678
+
679
+ tb.setMatchScore("m2", 0, 2);
680
+ tb.setMatchScore("m2", 1, 0);
681
+
682
+ tb.finishMatch("m2");
683
+
684
+ console.log(tb.getState());
685
+ ```
686
+
687
+ ---
688
+
689
+ ## Demo
690
+
691
+ The `demo/` folder can contain a live example of the library.
692
+
693
+ Recommended demo structure:
694
+
695
+ ```txt
696
+ demo/
697
+ index.html
698
+ demo.js
699
+ demo.css
700
+ ```
701
+
702
+ The demo should show both:
703
+
704
+ - the rendered tournament bracket
705
+ - the code used to create it
706
+
707
+ This way, users can see how the library works and copy the usage code from the same place.
708
+
709
+ ---
710
+
711
+ ## Styling
712
+
713
+ The library requires CSS for proper visual presentation.
714
+
715
+ ### Browser Usage
716
+
717
+ ```html
718
+ <link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
719
+ <script src="./dist/tournament-bracket.min.js"></script>
720
+ ```
721
+
722
+ ### ESM Usage
723
+
724
+ ```js
725
+ import "./dist/tournament-bracket.css";
726
+ import tournamentBracket from "./dist/tournament-bracket.esm.js";
727
+ ```
728
+
729
+ If the JavaScript works but the bracket looks broken or unstyled, the CSS file is probably missing.
730
+
731
+ Without CSS, the bracket technically works, but visually it may look like it lost a fight with a spreadsheet.
732
+
733
+ ---
734
+
735
+ ## Troubleshooting
736
+
737
+ ### The bracket does not render
738
+
739
+ Check that the target container exists:
740
+
741
+ ```html
742
+ <div id="tournament-bracket"></div>
743
+ ```
744
+
745
+ Also check that the `targetId` matches:
746
+
747
+ ```js
748
+ targetId: "tournament-bracket"
749
+ ```
750
+
751
+ ---
752
+
753
+ ### The browser says `tournamentBracket is not defined`
754
+
755
+ Make sure the browser build is loaded before your script:
756
+
757
+ ```html
758
+ <script src="./dist/tournament-bracket.min.js"></script>
759
+ <script>
760
+ const tb = tournamentBracket(config);
761
+ </script>
762
+ ```
763
+
764
+ ---
765
+
766
+ ### ESM import does not work
767
+
768
+ Make sure you are using the ESM build:
769
+
770
+ ```js
771
+ import tournamentBracket from "./dist/tournament-bracket.esm.js";
772
+ ```
773
+
774
+ If you are testing directly in the browser, use a local server and a module script:
775
+
776
+ ```html
777
+ <script type="module" src="./demo.js"></script>
778
+ ```
779
+
780
+ Do not rely on `file://` for ESM testing.
781
+
782
+ ---
783
+
784
+ ### Styles are missing
785
+
786
+ Include the CSS file:
787
+
788
+ ```html
789
+ <link rel="stylesheet" href="./dist/tournament-bracket.min.css" />
790
+ ```
791
+
792
+ or import it in your module setup:
793
+
794
+ ```js
795
+ import "./dist/tournament-bracket.css";
796
+ ```
797
+
798
+ ---
799
+
800
+ ### Invalid rounds error
801
+
802
+ Check these structure rules:
803
+
804
+ - first round match count must be a power of two
805
+ - each next round must have half the matches of the previous round
806
+ - final round must have exactly one match
807
+ - every match must have exactly two players
808
+
809
+ ---
810
+
811
+ ## Links
812
+
813
+ - [Live Demo](https://YOUR_USERNAME.github.io/tournament-bracket/demo/)
814
+ - [GitHub Repository](https://github.com/YOUR_USERNAME/tournament-bracket)
815
+ - [NPM Package](https://www.npmjs.com/package/tournament-bracket)
816
+
817
+ ---
818
+
819
+ ## License
820
+
821
+ MIT