@ethlete/cdk 4.61.1 → 4.61.2

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, TemplateRef, ViewContainerRef, Directive, input, booleanAttribute, computed, isDevMode, model, contentChild, effect, untracked, ChangeDetectionStrategy, ViewEncapsulation, Component, contentChildren, ElementRef, Injector, HostBinding, Input, numberAttribute, PLATFORM_ID, Renderer2, DestroyRef, signal, viewChild, viewChildren, ContentChild, Injectable, ChangeDetectorRef, ViewChild, ContentChildren, assertInInjectionContext, forwardRef, runInInjectionContext, EventEmitter, Output, ViewChildren, Optional, Inject, NgZone, HostListener, SkipSelf, NgModule, isSignal, IterableDiffers, ComponentFactoryResolver, HostAttributeToken } from '@angular/core';
2
+ import { InjectionToken, inject, TemplateRef, ViewContainerRef, Directive, input, booleanAttribute, computed, isDevMode, model, contentChild, effect, untracked, ChangeDetectionStrategy, ViewEncapsulation, Component, contentChildren, ElementRef, Injector, HostBinding, Input, numberAttribute, Renderer2, DOCUMENT, signal, viewChild, viewChildren, ContentChild, Injectable, ChangeDetectorRef, ViewChild, ContentChildren, assertInInjectionContext, forwardRef, runInInjectionContext, EventEmitter, Output, ViewChildren, Optional, Inject, NgZone, HostListener, SkipSelf, NgModule, DestroyRef, isSignal, IterableDiffers, ComponentFactoryResolver, HostAttributeToken } from '@angular/core';
3
3
  import * as i2 from '@angular/cdk/portal';
4
4
  import { CdkPortal, PortalModule, ComponentPortal, TemplatePortal, CdkPortalOutlet } from '@angular/cdk/portal';
5
5
  import { DomSanitizer, Title } from '@angular/platform-browser';
@@ -7,7 +7,7 @@ import { trigger, state, transition, style, animate } from '@angular/animations'
7
7
  import { toObservable, takeUntilDestroyed, toSignal, outputFromObservable } from '@angular/core/rxjs-interop';
8
8
  import { map, switchMap, combineLatest, pairwise, tap, BehaviorSubject, takeUntil, skip, of, merge, timer, takeWhile, filter, fromEvent, Subject, Observable, startWith, debounceTime, withLatestFrom, distinctUntilChanged, take, skipUntil, skipWhile, catchError, throwError, defer, partition, from, finalize, Subscription } from 'rxjs';
9
9
  import { __decorate, __metadata } from 'tslib';
10
- import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgClass, NgTemplateOutlet, DOCUMENT, Location, NgStyle } from '@angular/common';
10
+ import { AsyncPipe, NgComponentOutlet, NgClass, NgTemplateOutlet, DOCUMENT as DOCUMENT$1, Location, NgStyle } from '@angular/common';
11
11
  import * as i1 from '@ethlete/core';
12
12
  import { Memo, createComponentId, signalHostAttributes, signalHostClasses, createDestroy, previousSignalValue, signalHostStyles, nextFrame, syncSignal, signalStyles, injectHostElement, ObserveVisibilityDirective, signalVisibilityChangeClasses, IS_EMAIL, MUST_MATCH, IS_ARRAY_NOT_EMPTY, AT_LEAST_ONE_REQUIRED, equal, switchQueryListChanges, controlValueSignal, signalAttributes, ResizeObserverService, createFlipAnimation, AnimatedOverlayDirective, RuntimeError, SelectionModel, ActiveSelectionModel, KeyPressManager, signalClasses, scrollToElement, isEmptyArray, isObjectArray, isPrimitiveArray, ClickOutsideDirective, AnimatedLifecycleDirective, ANIMATED_LIFECYCLE_TOKEN, ObserveContentDirective, clamp, DELAYABLE_TOKEN, ObserveResizeDirective, SmartBlockScrollStrategy, signalElementDimensions, signalElementScrollState, createIsRenderedSignal, createCanAnimateSignal, useCursorDragScroll, signalElementIntersection, signalElementChildren, getIntersectionInfo, getElementScrollCoordinates, ClickObserverService, RootBoundaryDirective, elementCanScroll, cloneFormGroup, getFormGroupValue, ViewportService, injectRoute, ROOT_BOUNDARY_TOKEN, injectQueryParam, RouterStateService, fromNextFrame, isElementVisible, AnimatedIfDirective, FocusVisibleService, inferMimeType, TypedQueryList } from '@ethlete/core';
13
13
  import { extractQuery, isQueryStateSuccess, isQueryStateFailure, isQueryStateLoading, QueryDirective, queryComputed, switchQueryState, shouldRetryRequest, ExperimentalQuery, isClassValidatorError, isSymfonyFormViolationListError, isSymfonyListError } from '@ethlete/query';
@@ -238,13 +238,13 @@ class AccordionComponent {
238
238
  this.isOpen.set(false);
239
239
  }
240
240
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
241
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: AccordionComponent, isStandalone: true, selector: "et-accordion", inputs: { isOpenByDefault: { classPropertyName: "isOpenByDefault", publicName: "isOpenByDefault", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, host: { classAttribute: "et-accordion" }, providers: [{ provide: ACCORDION_COMPONENT, useExisting: AccordionComponent }, provideIcons(CHEVRON_ICON)], queries: [{ propertyName: "templateLabel", first: true, predicate: ACCORDION_LABEL_WRAPPER_DIRECTIVE, descendants: true, isSignal: true }, { propertyName: "templateHint", first: true, predicate: ACCORDION_HINT_WRAPPER_DIRECTIVE, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"et-accordion-container\">\n <h3 class=\"et-accordion-header-wrapper\">\n <button\n [disabled]=\"disabled()\"\n [id]=\"HEADER_ID\"\n [attr.aria-controls]=\"BODY_ID\"\n [attr.aria-expanded]=\"isOpen()\"\n [class.et-accordion-has-hint]=\"!!templateHint\"\n (click)=\"toggleAccordionOpen()\"\n class=\"et-accordion-header\"\n type=\"button\"\n >\n <div>\n @if (templateLabel(); as templateLabel) {\n <ng-template [cdkPortalOutlet]=\"templateLabel\" />\n } @else {\n <span et-accordion-label>{{ label() }} </span>\n }\n </div>\n\n @if (templateHint(); as templateHint) {\n <ng-template [cdkPortalOutlet]=\"templateHint\" />\n }\n\n <i etIcon=\"et-chevron\"> </i>\n </button>\n </h3>\n <div\n [@animateOpenClose]=\"isOpen() ? 'open' : 'close'\"\n [attr.aria-labelledby]=\"HEADER_ID\"\n [id]=\"BODY_ID\"\n class=\"et-accordion-body\"\n role=\"region\"\n >\n <div class=\"et-accordion-body-container\">\n <ng-content />\n </div>\n </div>\n <hr class=\"et-accordion-separator\" />\n</div>\n", styles: [":where(.et-accordion){--chevron-size: 15px}.et-accordion .et-accordion-container{position:relative;z-index:1}.et-accordion .et-icon--et-chevron{display:block;transform:rotate(180deg);transition:transform .3s var(--ease-5);inline-size:var(--chevron-size);block-size:var(--chevron-size)}.et-accordion [aria-expanded=true] .et-icon--et-chevron{transform:rotate(0)}.et-accordion .et-accordion-header{display:grid;grid-template-columns:1fr auto;inline-size:100%;block-size:100%;text-align:left;background-color:transparent;border:none;align-items:center;padding:0;gap:15px}.et-accordion .et-accordion-header:not(:disabled){cursor:pointer}.et-accordion .et-accordion-header.et-accordion-has-hint{grid-template-columns:1fr auto auto}.et-accordion .et-accordion-header-wrapper{margin:0}.et-accordion .et-accordion-body{overflow:hidden}.et-accordion .et-accordion-separator{position:relative;z-index:-1}\n"], dependencies: [{ kind: "directive", type: AccordionLabelDirective, selector: "[et-accordion-label]" }, { kind: "ngmodule", type: PortalModule }, { kind: "directive", type: i2.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "directive", type: IconDirective, selector: "[etIcon]", inputs: ["etIcon", "allowHardcodedColor"] }], animations: [accordionAnimations.animateOpenClose], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
241
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: AccordionComponent, isStandalone: true, selector: "et-accordion", inputs: { isOpenByDefault: { classPropertyName: "isOpenByDefault", publicName: "isOpenByDefault", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, host: { classAttribute: "et-accordion" }, providers: [{ provide: ACCORDION_COMPONENT, useExisting: AccordionComponent }, provideIcons(CHEVRON_ICON)], queries: [{ propertyName: "templateLabel", first: true, predicate: ACCORDION_LABEL_WRAPPER_DIRECTIVE, descendants: true, isSignal: true }, { propertyName: "templateHint", first: true, predicate: ACCORDION_HINT_WRAPPER_DIRECTIVE, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"et-accordion-container\">\n <h3 class=\"et-accordion-header-wrapper\">\n <button\n [disabled]=\"disabled()\"\n [id]=\"HEADER_ID\"\n [attr.aria-controls]=\"BODY_ID\"\n [attr.aria-expanded]=\"isOpen()\"\n [class.et-accordion-has-hint]=\"!!templateHint()\"\n (click)=\"toggleAccordionOpen()\"\n class=\"et-accordion-header\"\n type=\"button\"\n >\n <div>\n @if (templateLabel(); as templateLabel) {\n <ng-template [cdkPortalOutlet]=\"templateLabel\" />\n } @else {\n <span et-accordion-label>{{ label() }} </span>\n }\n </div>\n\n @if (templateHint(); as templateHint) {\n <ng-template [cdkPortalOutlet]=\"templateHint\" />\n }\n <i etIcon=\"et-chevron\"> </i>\n </button>\n </h3>\n\n <div\n [@animateOpenClose]=\"isOpen() ? 'open' : 'close'\"\n [attr.aria-labelledby]=\"HEADER_ID\"\n [id]=\"BODY_ID\"\n class=\"et-accordion-body\"\n role=\"region\"\n >\n <div class=\"et-accordion-body-container\"><ng-content /></div>\n </div>\n\n <hr class=\"et-accordion-separator\" />\n</div>\n", styles: [":where(.et-accordion){--chevron-size: 15px}.et-accordion .et-accordion-container{position:relative;z-index:1}.et-accordion .et-icon--et-chevron{display:block;transform:rotate(180deg);transition:transform .3s var(--ease-5);inline-size:var(--chevron-size);block-size:var(--chevron-size)}.et-accordion [aria-expanded=true] .et-icon--et-chevron{transform:rotate(0)}.et-accordion .et-accordion-header{display:grid;grid-template-columns:1fr auto;inline-size:100%;block-size:100%;text-align:left;background-color:transparent;border:none;align-items:center;padding:0;gap:15px}.et-accordion .et-accordion-header:not(:disabled){cursor:pointer}.et-accordion .et-accordion-header.et-accordion-has-hint{grid-template-columns:1fr auto auto}.et-accordion .et-accordion-header-wrapper{margin:0}.et-accordion .et-accordion-body{overflow:hidden}.et-accordion .et-accordion-separator{position:relative;z-index:-1}\n"], dependencies: [{ kind: "directive", type: AccordionLabelDirective, selector: "[et-accordion-label]" }, { kind: "ngmodule", type: PortalModule }, { kind: "directive", type: i2.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "directive", type: IconDirective, selector: "[etIcon]", inputs: ["etIcon", "allowHardcodedColor"] }], animations: [accordionAnimations.animateOpenClose], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
242
242
  }
243
243
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AccordionComponent, decorators: [{
244
244
  type: Component,
245
245
  args: [{ selector: 'et-accordion', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: ACCORDION_COMPONENT, useExisting: AccordionComponent }, provideIcons(CHEVRON_ICON)], imports: [AccordionLabelDirective, PortalModule, IconDirective], animations: [accordionAnimations.animateOpenClose], host: {
246
246
  class: 'et-accordion',
247
- }, template: "<div class=\"et-accordion-container\">\n <h3 class=\"et-accordion-header-wrapper\">\n <button\n [disabled]=\"disabled()\"\n [id]=\"HEADER_ID\"\n [attr.aria-controls]=\"BODY_ID\"\n [attr.aria-expanded]=\"isOpen()\"\n [class.et-accordion-has-hint]=\"!!templateHint\"\n (click)=\"toggleAccordionOpen()\"\n class=\"et-accordion-header\"\n type=\"button\"\n >\n <div>\n @if (templateLabel(); as templateLabel) {\n <ng-template [cdkPortalOutlet]=\"templateLabel\" />\n } @else {\n <span et-accordion-label>{{ label() }} </span>\n }\n </div>\n\n @if (templateHint(); as templateHint) {\n <ng-template [cdkPortalOutlet]=\"templateHint\" />\n }\n\n <i etIcon=\"et-chevron\"> </i>\n </button>\n </h3>\n <div\n [@animateOpenClose]=\"isOpen() ? 'open' : 'close'\"\n [attr.aria-labelledby]=\"HEADER_ID\"\n [id]=\"BODY_ID\"\n class=\"et-accordion-body\"\n role=\"region\"\n >\n <div class=\"et-accordion-body-container\">\n <ng-content />\n </div>\n </div>\n <hr class=\"et-accordion-separator\" />\n</div>\n", styles: [":where(.et-accordion){--chevron-size: 15px}.et-accordion .et-accordion-container{position:relative;z-index:1}.et-accordion .et-icon--et-chevron{display:block;transform:rotate(180deg);transition:transform .3s var(--ease-5);inline-size:var(--chevron-size);block-size:var(--chevron-size)}.et-accordion [aria-expanded=true] .et-icon--et-chevron{transform:rotate(0)}.et-accordion .et-accordion-header{display:grid;grid-template-columns:1fr auto;inline-size:100%;block-size:100%;text-align:left;background-color:transparent;border:none;align-items:center;padding:0;gap:15px}.et-accordion .et-accordion-header:not(:disabled){cursor:pointer}.et-accordion .et-accordion-header.et-accordion-has-hint{grid-template-columns:1fr auto auto}.et-accordion .et-accordion-header-wrapper{margin:0}.et-accordion .et-accordion-body{overflow:hidden}.et-accordion .et-accordion-separator{position:relative;z-index:-1}\n"] }]
247
+ }, template: "<div class=\"et-accordion-container\">\n <h3 class=\"et-accordion-header-wrapper\">\n <button\n [disabled]=\"disabled()\"\n [id]=\"HEADER_ID\"\n [attr.aria-controls]=\"BODY_ID\"\n [attr.aria-expanded]=\"isOpen()\"\n [class.et-accordion-has-hint]=\"!!templateHint()\"\n (click)=\"toggleAccordionOpen()\"\n class=\"et-accordion-header\"\n type=\"button\"\n >\n <div>\n @if (templateLabel(); as templateLabel) {\n <ng-template [cdkPortalOutlet]=\"templateLabel\" />\n } @else {\n <span et-accordion-label>{{ label() }} </span>\n }\n </div>\n\n @if (templateHint(); as templateHint) {\n <ng-template [cdkPortalOutlet]=\"templateHint\" />\n }\n <i etIcon=\"et-chevron\"> </i>\n </button>\n </h3>\n\n <div\n [@animateOpenClose]=\"isOpen() ? 'open' : 'close'\"\n [attr.aria-labelledby]=\"HEADER_ID\"\n [id]=\"BODY_ID\"\n class=\"et-accordion-body\"\n role=\"region\"\n >\n <div class=\"et-accordion-body-container\"><ng-content /></div>\n </div>\n\n <hr class=\"et-accordion-separator\" />\n</div>\n", styles: [":where(.et-accordion){--chevron-size: 15px}.et-accordion .et-accordion-container{position:relative;z-index:1}.et-accordion .et-icon--et-chevron{display:block;transform:rotate(180deg);transition:transform .3s var(--ease-5);inline-size:var(--chevron-size);block-size:var(--chevron-size)}.et-accordion [aria-expanded=true] .et-icon--et-chevron{transform:rotate(0)}.et-accordion .et-accordion-header{display:grid;grid-template-columns:1fr auto;inline-size:100%;block-size:100%;text-align:left;background-color:transparent;border:none;align-items:center;padding:0;gap:15px}.et-accordion .et-accordion-header:not(:disabled){cursor:pointer}.et-accordion .et-accordion-header.et-accordion-has-hint{grid-template-columns:1fr auto auto}.et-accordion .et-accordion-header-wrapper{margin:0}.et-accordion .et-accordion-body{overflow:hidden}.et-accordion .et-accordion-separator{position:relative;z-index:-1}\n"] }]
248
248
  }], ctorParameters: () => [] });
249
249
 
250
250
  class AccordionGroupComponent {
@@ -1032,7 +1032,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImpor
1032
1032
 
1033
1033
  const BracketImports = [BracketComponent, BracketMatchComponent, BracketRoundHeaderComponent];
1034
1034
 
1035
- const FALLBACK_MATCH_POSITION = -1;
1036
1035
  const TOURNAMENT_MODE = {
1037
1036
  SINGLE_ELIMINATION: 'single-elimination',
1038
1037
  DOUBLE_ELIMINATION: 'double-elimination',
@@ -1040,6 +1039,45 @@ const TOURNAMENT_MODE = {
1040
1039
  SWISS: 'swiss',
1041
1040
  SWISS_WITH_ELIMINATION: 'swiss-with-elimination',
1042
1041
  };
1042
+
1043
+ const canRenderLayoutInTournamentMode = (layout, mode) => {
1044
+ switch (mode) {
1045
+ case TOURNAMENT_MODE.SINGLE_ELIMINATION:
1046
+ return layout === BRACKET_DATA_LAYOUT.LEFT_TO_RIGHT || layout === BRACKET_DATA_LAYOUT.MIRRORED;
1047
+ default:
1048
+ return layout === BRACKET_DATA_LAYOUT.LEFT_TO_RIGHT;
1049
+ }
1050
+ };
1051
+ const BRACKET_DATA_LAYOUT = {
1052
+ LEFT_TO_RIGHT: 'left-to-right',
1053
+ // Currently only supported in single elimination brackets. Will throw an error if used in other bracket types
1054
+ MIRRORED: 'mirrored',
1055
+ };
1056
+
1057
+ class BracketMap extends Map {
1058
+ constructor() {
1059
+ super();
1060
+ }
1061
+ getOrThrow(key) {
1062
+ const value = super.get(key);
1063
+ if (value === undefined) {
1064
+ throw new Error(`Value for key ${key} not found in bracket map`);
1065
+ }
1066
+ return value;
1067
+ }
1068
+ first() {
1069
+ return this.values().next().value;
1070
+ }
1071
+ last() {
1072
+ const values = Array.from(this.values());
1073
+ return values.length > 0 ? values[values.length - 1] : undefined;
1074
+ }
1075
+ }
1076
+
1077
+ const BRACKET_ROUND_MIRROR_TYPE = {
1078
+ LEFT: 'left',
1079
+ RIGHT: 'right',
1080
+ };
1043
1081
  const COMMON_BRACKET_ROUND_TYPE = {
1044
1082
  THIRD_PLACE: 'third-place',
1045
1083
  FINAL: 'final',
@@ -1058,10 +1096,283 @@ const SWISS_BRACKET_ROUND_TYPE = {
1058
1096
  const GROUP_BRACKET_ROUND_TYPE = {
1059
1097
  GROUP: 'group',
1060
1098
  };
1061
- const BRACKET_ROUND_MIRROR_TYPE = {
1062
- LEFT: 'left',
1063
- RIGHT: 'right',
1099
+ const createRoundsMapBase = (source, options) => {
1100
+ const map = new BracketMap();
1101
+ const shouldSplitRoundsInTwo = options.layout === BRACKET_DATA_LAYOUT.MIRRORED;
1102
+ const isDoubleElimination = source.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION;
1103
+ let currentUpperBracketIndex = 0;
1104
+ let currentLowerBracketIndex = 0;
1105
+ const splitRoundsRest = [];
1106
+ for (const [roundIndex, round] of source.rounds.entries()) {
1107
+ const isUpperBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET;
1108
+ const isLowerBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
1109
+ const isCommonDoubleEliminationRound = !isUpperBracket && !isLowerBracket && isDoubleElimination;
1110
+ const matches = source.matches.filter((m) => m.roundId === round.id);
1111
+ const roundId = round.id;
1112
+ const shouldSplitRound = shouldSplitRoundsInTwo && matches.length % 2 === 0;
1113
+ const isFirstRound = roundIndex === 0;
1114
+ const isLastRound = roundIndex === source.rounds.length - 1;
1115
+ const isFirstOfType = source.rounds.findIndex((r) => r.type === round.type) === roundIndex;
1116
+ const isLastOfType = source.rounds
1117
+ .slice()
1118
+ .reverse()
1119
+ .findIndex((r) => r.type === round.type) ===
1120
+ source.rounds.length - roundIndex - 1;
1121
+ if (shouldSplitRound) {
1122
+ const firstHalfRoundId = `${roundId}--half-1`;
1123
+ const secondHalfRoundId = `${roundId}--half-2`;
1124
+ const firstHalfMatchesMaxIndex = matches.length / 2 - 1;
1125
+ const bracketRoundFirstHalf = {
1126
+ type: round.type,
1127
+ id: firstHalfRoundId,
1128
+ logicalIndex: isLowerBracket || isCommonDoubleEliminationRound ? currentLowerBracketIndex : currentUpperBracketIndex,
1129
+ data: round.data,
1130
+ position: ((isLowerBracket || isCommonDoubleEliminationRound
1131
+ ? currentLowerBracketIndex
1132
+ : currentUpperBracketIndex) + 1),
1133
+ name: round.name,
1134
+ matchCount: matches.length / 2,
1135
+ matchIds: matches.slice(0, firstHalfMatchesMaxIndex + 1).map((m) => m.id),
1136
+ mirrorRoundType: BRACKET_ROUND_MIRROR_TYPE.LEFT,
1137
+ isFirstRound,
1138
+ isLastRound,
1139
+ isFirstOfType,
1140
+ isLastOfType,
1141
+ };
1142
+ const bracketRoundSecondHalf = {
1143
+ type: round.type,
1144
+ id: secondHalfRoundId,
1145
+ logicalIndex: -1,
1146
+ data: round.data,
1147
+ position: -1,
1148
+ name: round.name,
1149
+ matchCount: matches.length / 2,
1150
+ matchIds: matches.slice(firstHalfMatchesMaxIndex + 1).map((m) => m.id),
1151
+ mirrorRoundType: BRACKET_ROUND_MIRROR_TYPE.RIGHT,
1152
+ isFirstRound,
1153
+ isLastRound,
1154
+ isFirstOfType,
1155
+ isLastOfType,
1156
+ };
1157
+ map.set(firstHalfRoundId, bracketRoundFirstHalf);
1158
+ splitRoundsRest.unshift(bracketRoundSecondHalf);
1159
+ }
1160
+ else {
1161
+ const bracketRound = {
1162
+ type: round.type,
1163
+ id: roundId,
1164
+ logicalIndex: isLowerBracket || isCommonDoubleEliminationRound ? currentLowerBracketIndex : currentUpperBracketIndex,
1165
+ data: round.data,
1166
+ position: ((isLowerBracket || isCommonDoubleEliminationRound
1167
+ ? currentLowerBracketIndex
1168
+ : currentUpperBracketIndex) + 1),
1169
+ name: round.name,
1170
+ matchCount: matches.length,
1171
+ matchIds: matches.map((m) => m.id),
1172
+ mirrorRoundType: null,
1173
+ isFirstRound,
1174
+ isLastRound,
1175
+ isFirstOfType,
1176
+ isLastOfType,
1177
+ };
1178
+ map.set(roundId, bracketRound);
1179
+ }
1180
+ if (isLowerBracket || isCommonDoubleEliminationRound) {
1181
+ currentLowerBracketIndex++;
1182
+ }
1183
+ else {
1184
+ currentUpperBracketIndex++;
1185
+ }
1186
+ }
1187
+ if (splitRoundsRest.length) {
1188
+ const lastRound = map.last();
1189
+ if (!lastRound)
1190
+ throw new Error('Last round not found');
1191
+ for (const [splitRoundIndex, splitRound] of splitRoundsRest.entries()) {
1192
+ splitRound.logicalIndex = lastRound.logicalIndex + splitRoundIndex + 1;
1193
+ splitRound.position = (lastRound.position + splitRoundIndex + 1);
1194
+ map.set(splitRound.id, splitRound);
1195
+ }
1196
+ }
1197
+ return map;
1198
+ };
1199
+
1200
+ const createNewMatchParticipantBase = (source, participantId, match, rounds, matchRoundId, participants) => {
1201
+ if (!participantId)
1202
+ return null;
1203
+ const participantBase = participants.getOrThrow(participantId);
1204
+ const roundBase = rounds.getOrThrow(matchRoundId);
1205
+ const matchIndex = participantBase.matchIds.indexOf(match.id);
1206
+ if (matchIndex === -1)
1207
+ throw new Error(`Match with id ${match.id} not found in participant with id ${participantId}`);
1208
+ const participantSide = match.home === participantId ? 'home' : 'away';
1209
+ const isWinner = match.winner === participantSide;
1210
+ const isLooser = match.winner && match.winner !== participantSide;
1211
+ const isTie = match.status === 'completed' && !match.winner;
1212
+ let winsTilNow = isWinner ? 1 : 0;
1213
+ let lossesTilNow = isLooser ? 1 : 0;
1214
+ let tiesTilNow = isTie ? 1 : 0;
1215
+ for (let i = 0; i < matchIndex; i++) {
1216
+ const previousMatchId = participantBase.matchIds[i];
1217
+ const previousMatch = source.matches.find((m) => m.id === previousMatchId);
1218
+ const myPreviousMatchSide = previousMatch?.home === participantId ? 'home' : previousMatch?.away === participantId ? 'away' : null;
1219
+ if (!previousMatch || !myPreviousMatchSide)
1220
+ continue;
1221
+ const previousIsWinner = previousMatch.winner === myPreviousMatchSide;
1222
+ const previousIsLooser = previousMatch.winner && previousMatch.winner !== myPreviousMatchSide;
1223
+ const previousIsTie = previousMatch.status === 'completed' && !previousMatch.winner;
1224
+ if (previousIsWinner) {
1225
+ winsTilNow++;
1226
+ }
1227
+ else if (previousIsLooser) {
1228
+ lossesTilNow++;
1229
+ }
1230
+ else if (previousIsTie) {
1231
+ tiesTilNow++;
1232
+ }
1233
+ }
1234
+ const hasElimination = source.mode === TOURNAMENT_MODE.SINGLE_ELIMINATION ||
1235
+ source.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION ||
1236
+ source.mode === TOURNAMENT_MODE.SWISS_WITH_ELIMINATION;
1237
+ // Means the current match is loss and it's the last match of the participant
1238
+ let isEliminated = false;
1239
+ // Always true for single elimination, never for e.g. groups, depends on the round for double elimination, depends on the loss count for swiss with elimination
1240
+ let isEliminationMatch = false;
1241
+ if (hasElimination) {
1242
+ switch (source.mode) {
1243
+ case TOURNAMENT_MODE.SINGLE_ELIMINATION: {
1244
+ isEliminationMatch = true;
1245
+ isEliminated = isLooser ?? false;
1246
+ break;
1247
+ }
1248
+ case TOURNAMENT_MODE.DOUBLE_ELIMINATION: {
1249
+ isEliminationMatch =
1250
+ roundBase.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET ||
1251
+ roundBase.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL ||
1252
+ roundBase.type === COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE;
1253
+ // Final is only an elimination match if there is no reverse final or there is one and this participant came from the lower bracket
1254
+ if (roundBase.type === COMMON_BRACKET_ROUND_TYPE.FINAL) {
1255
+ const hasReverseFinal = source.rounds.some((r) => r.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL);
1256
+ if (!hasReverseFinal)
1257
+ break;
1258
+ const currentMatchIndex = participantBase.matchIds.indexOf(match.id);
1259
+ if (currentMatchIndex === -1)
1260
+ throw new Error(`Match with id ${match.id} not found in participant with id ${participantId}`);
1261
+ if (currentMatchIndex === 0)
1262
+ break;
1263
+ const previousMatchIndex = currentMatchIndex - 1;
1264
+ const previousMatchId = participantBase.matchIds[previousMatchIndex];
1265
+ const previousMatch = source.matches.find((m) => m.id === previousMatchId);
1266
+ if (!previousMatch)
1267
+ throw new Error(`Previous match with id ${previousMatchId} not found`);
1268
+ const previousRound = rounds.getOrThrow(previousMatch.roundId);
1269
+ isEliminationMatch = previousRound.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
1270
+ }
1271
+ isEliminated = (isEliminationMatch && isLooser) ?? false;
1272
+ break;
1273
+ }
1274
+ case TOURNAMENT_MODE.SWISS_WITH_ELIMINATION: {
1275
+ // In swiss you are eliminated after 3 losses, so we need to track the loss count
1276
+ isEliminationMatch = lossesTilNow >= 2;
1277
+ isEliminated = (isEliminationMatch && isLooser) ?? false;
1278
+ }
1279
+ }
1280
+ }
1281
+ const matchParticipantBase = {
1282
+ ...participantBase,
1283
+ isEliminated,
1284
+ isEliminationMatch,
1285
+ lossCount: lossesTilNow,
1286
+ tieCount: tiesTilNow,
1287
+ winCount: winsTilNow,
1288
+ result: isWinner ? 'win' : isLooser ? 'loss' : isTie ? 'tie' : null,
1289
+ side: participantSide,
1290
+ };
1291
+ return matchParticipantBase;
1292
+ };
1293
+
1294
+ const createMatchesMapBase = (source, rounds, participants) => {
1295
+ const map = new BracketMap();
1296
+ for (const match of source.matches) {
1297
+ const genericRound = rounds.get(match.roundId);
1298
+ const splitRound = rounds.get(`${match.roundId}--half-1`);
1299
+ const splitRound2 = rounds.get(`${match.roundId}--half-2`);
1300
+ const genericRoundMatchIndex = genericRound?.matchIds.indexOf(match.id) ?? -1;
1301
+ const splitRoundMatchIndex = splitRound?.matchIds.indexOf(match.id) ?? -1;
1302
+ const splitRound2MatchIndex = splitRound2?.matchIds.indexOf(match.id) ?? -1;
1303
+ const roundToUse = genericRoundMatchIndex !== -1
1304
+ ? genericRound
1305
+ : splitRoundMatchIndex !== -1
1306
+ ? splitRound
1307
+ : splitRound2MatchIndex !== -1
1308
+ ? splitRound2
1309
+ : null;
1310
+ if (!roundToUse)
1311
+ throw new Error(`Round for match with id ${match.id} not found`);
1312
+ const indexInRound = genericRoundMatchIndex !== -1
1313
+ ? genericRoundMatchIndex
1314
+ : splitRoundMatchIndex !== -1
1315
+ ? splitRoundMatchIndex
1316
+ : splitRound2MatchIndex !== -1
1317
+ ? splitRound2MatchIndex
1318
+ : -1;
1319
+ if (indexInRound === -1)
1320
+ throw new Error(`Match with id ${match.id} not found in round with id ${roundToUse.id}`);
1321
+ const home = createNewMatchParticipantBase(source, match.home, match, rounds, roundToUse.id, participants);
1322
+ const away = createNewMatchParticipantBase(source, match.away, match, rounds, roundToUse.id, participants);
1323
+ const winner = match.winner === 'home' ? home : match.winner === 'away' ? away : null;
1324
+ const matchBase = {
1325
+ home,
1326
+ away,
1327
+ data: match.data,
1328
+ id: match.id,
1329
+ indexInRound,
1330
+ position: (indexInRound + 1),
1331
+ roundId: roundToUse?.id,
1332
+ status: match.status,
1333
+ winner,
1334
+ winnerSide: match.winner,
1335
+ };
1336
+ map.set(matchBase.id, matchBase);
1337
+ }
1338
+ return map;
1339
+ };
1340
+
1341
+ const createParticipantsMapBase = (source) => {
1342
+ const map = new BracketMap();
1343
+ const participantIds = new Set(source.matches
1344
+ .map((m) => [m.home, m.away])
1345
+ .flat()
1346
+ .filter((p) => !!p));
1347
+ for (const [index, participantId] of participantIds.entries()) {
1348
+ const participantBase = {
1349
+ id: participantId,
1350
+ shortId: `p${index}`,
1351
+ matchIds: source.matches
1352
+ .filter((m) => m.home === participantId || m.away === participantId)
1353
+ .map((m) => m.id),
1354
+ };
1355
+ map.set(participantId, participantBase);
1356
+ }
1357
+ return map;
1358
+ };
1359
+
1360
+ const createNewBracketBase = (source, options) => {
1361
+ if (!canRenderLayoutInTournamentMode(options.layout, source.mode)) {
1362
+ throw new Error(`Cannot render layout ${options.layout} in mode ${source.mode}`);
1363
+ }
1364
+ const participants = createParticipantsMapBase(source);
1365
+ const rounds = createRoundsMapBase(source, options);
1366
+ const matches = createMatchesMapBase(source, rounds, participants);
1367
+ const bracketData = {
1368
+ matches,
1369
+ rounds,
1370
+ participants,
1371
+ mode: source.mode,
1372
+ };
1373
+ return bracketData;
1064
1374
  };
1375
+
1065
1376
  const generateRoundTypeFromEthleteRoundType = (type, tournamentMode, roundMatchCount) => {
1066
1377
  switch (type) {
1067
1378
  case 'normal':
@@ -1122,1029 +1433,976 @@ const generateTournamentModeFormEthleteRounds = (source) => {
1122
1433
  throw new Error(`Unsupported tournament mode: ${firstMatch.matchType}`);
1123
1434
  }
1124
1435
  };
1125
- const canRenderLayoutInTournamentMode = (layout, mode) => {
1126
- switch (mode) {
1127
- case TOURNAMENT_MODE.SINGLE_ELIMINATION:
1128
- return layout === BRACKET_DATA_LAYOUT.LEFT_TO_RIGHT || layout === BRACKET_DATA_LAYOUT.MIRRORED;
1129
- default:
1130
- return layout === BRACKET_DATA_LAYOUT.LEFT_TO_RIGHT;
1131
- }
1132
- };
1133
- const BRACKET_DATA_LAYOUT = {
1134
- LEFT_TO_RIGHT: 'left-to-right',
1135
- // Currently only supported in single elimination brackets. Will throw an error if used in other bracket types
1136
- MIRRORED: 'mirrored',
1137
- };
1138
- const FIRST_ROUNDS_TYPE = {
1139
- SINGLE: 'single',
1140
- DOUBLE: 'double',
1141
- };
1142
- const getFirstRounds = (bracketData, roundTypeMap) => {
1143
- if (bracketData.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION) {
1144
- const upper = roundTypeMap.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)?.values().next().value;
1145
- const lower = roundTypeMap.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)?.values().next().value;
1146
- if (!upper || !lower)
1147
- throw new Error('Upper or lower bracket is null');
1148
- return {
1149
- type: FIRST_ROUNDS_TYPE.DOUBLE,
1150
- upper,
1151
- lower,
1436
+ const generateBracketDataForEthlete = (source) => {
1437
+ const tournamentMode = generateTournamentModeFormEthleteRounds(source);
1438
+ const bracketData = {
1439
+ rounds: [],
1440
+ matches: [],
1441
+ mode: tournamentMode,
1442
+ };
1443
+ for (const currentItem of source) {
1444
+ if (bracketData.rounds.some((r) => r.id === currentItem.round.id)) {
1445
+ throw new Error(`Round with id ${currentItem.round.id} already exists in the bracket data.`);
1446
+ }
1447
+ const roundType = generateRoundTypeFromEthleteRoundType(currentItem.round.type, tournamentMode, currentItem.matches.length);
1448
+ const bracketRound = {
1449
+ type: roundType,
1450
+ id: currentItem.round.id,
1451
+ data: currentItem.round,
1452
+ name: currentItem.round.name || currentItem.round.type,
1152
1453
  };
1454
+ bracketData.rounds.push(bracketRound);
1455
+ for (const match of currentItem.matches) {
1456
+ if (bracketData.matches.some((m) => m.id === match.id)) {
1457
+ throw new Error(`Match with id ${match.id} already exists in the bracket data.`);
1458
+ }
1459
+ const bracketMatch = {
1460
+ id: match.id,
1461
+ data: match,
1462
+ roundId: currentItem.round.id,
1463
+ home: match.home?.id || null,
1464
+ away: match.away?.id || null,
1465
+ winner: match.winningSide,
1466
+ status: match.status === 'published' ? 'completed' : 'pending',
1467
+ };
1468
+ bracketData.matches.push(bracketMatch);
1469
+ }
1153
1470
  }
1154
- const first = bracketData.rounds.values().next().value;
1155
- if (!first)
1156
- throw new Error('First round is null');
1157
- return {
1158
- type: FIRST_ROUNDS_TYPE.SINGLE,
1159
- first,
1160
- };
1471
+ return bracketData;
1161
1472
  };
1162
- const generateMatchPositionMaps = (bracketData) => {
1163
- const matchPositionMaps = new Map();
1164
- for (const round of bracketData.rounds.values()) {
1165
- const matchMap = new Map([...round.matches.values()].map((m) => [m.position, m]));
1166
- matchPositionMaps.set(round.id, matchMap);
1473
+
1474
+ const generateTournamentModeFormGgData = (source) => {
1475
+ switch (source.mode) {
1476
+ // case 'groups':
1477
+ // return TOURNAMENT_MODE.GROUP;
1478
+ // case 'fifa_swiss': {
1479
+ // const lastRound = source[source.length - 1];
1480
+ // if (!lastRound) throw new Error('No last round found');
1481
+ // if (lastRound.matches.length !== firstRound.matches.length) {
1482
+ // return TOURNAMENT_MODE.SWISS_WITH_ELIMINATION;
1483
+ // } else {
1484
+ // return TOURNAMENT_MODE.SWISS;
1485
+ // }
1486
+ // }
1487
+ case 'double-elimination':
1488
+ return TOURNAMENT_MODE.DOUBLE_ELIMINATION;
1489
+ case 'single-elimination':
1490
+ return TOURNAMENT_MODE.SINGLE_ELIMINATION;
1491
+ default:
1492
+ throw new Error(`Unsupported tournament mode: ${source.mode}`);
1167
1493
  }
1168
- return matchPositionMaps;
1169
1494
  };
1170
- const generateBracketRoundTypeMap = (bracketData) => {
1171
- const roundAmountMap = new Map();
1172
- for (const round of bracketData.rounds.values()) {
1173
- if (!roundAmountMap.has(round.type)) {
1174
- roundAmountMap.set(round.type, new Map([[round.id, round]]));
1175
- }
1176
- else {
1177
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1178
- roundAmountMap.set(round.type, roundAmountMap.get(round.type).set(round.id, round));
1495
+ const generateRoundTypeFromGgMatch = (tournamentMode, bracketType, // 'winner' | 'looser' | null,
1496
+ stageNumber, matchCount) => {
1497
+ switch (tournamentMode) {
1498
+ case 'double-elimination': {
1499
+ switch (stageNumber) {
1500
+ case 1: {
1501
+ switch (bracketType) {
1502
+ case 'winner':
1503
+ return DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET;
1504
+ case 'looser':
1505
+ return DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
1506
+ default:
1507
+ throw new Error(`Unsupported bracket type: ${bracketType}`);
1508
+ }
1509
+ }
1510
+ case 2: {
1511
+ return COMMON_BRACKET_ROUND_TYPE.FINAL;
1512
+ }
1513
+ case 3: {
1514
+ return DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL;
1515
+ }
1516
+ default: {
1517
+ throw new Error(`Unsupported stage number: ${stageNumber}`);
1518
+ }
1519
+ }
1179
1520
  }
1180
- }
1181
- return roundAmountMap;
1182
- };
1183
- const generateRoundRelations = (bracketData) => {
1184
- const roundRelations = new Map();
1185
- const allRounds = bracketData.roundIds.map((id) => bracketData.rounds.get(id)).filter((r) => !!r);
1186
- const upperRounds = allRounds.filter((r) => r.type !== DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET);
1187
- const lowerRounds = allRounds.filter((r) => r.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET);
1188
- const firstUpperRound = upperRounds[0] || null;
1189
- const firstLowerRound = lowerRounds[0] || null;
1190
- if (!firstUpperRound)
1191
- throw new Error('firstUpperRound is null');
1192
- const hasLowerRounds = lowerRounds.length > 0;
1193
- const lastLowerRound = lowerRounds[lowerRounds.length - 1] || null;
1194
- for (const [currentUpperRoundIndex, currentUpperRound] of upperRounds.entries()) {
1195
- const isLeftToRight = currentUpperRound.mirrorRoundType === null ||
1196
- currentUpperRound.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.LEFT;
1197
- const relativePreviousUpperRound = upperRounds[currentUpperRoundIndex - 1] || null;
1198
- const relativeNextUpperRound = upperRounds[currentUpperRoundIndex + 1] || null;
1199
- const previousUpperRound = isLeftToRight ? relativePreviousUpperRound : relativeNextUpperRound;
1200
- const nextUpperRound = isLeftToRight ? relativeNextUpperRound : relativePreviousUpperRound;
1201
- const currentLowerRound = lowerRounds[currentUpperRoundIndex] || null;
1202
- const previousLowerRound = lowerRounds[currentUpperRoundIndex - 1] || null;
1203
- const nextLowerRound = lowerRounds[currentUpperRoundIndex + 1] || null;
1204
- const isLastUpperRound = !nextUpperRound ||
1205
- (nextUpperRound.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.RIGHT &&
1206
- currentUpperRound.mirrorRoundType === null);
1207
- const isFinal = currentUpperRound.type === COMMON_BRACKET_ROUND_TYPE.FINAL;
1208
- if (isFinal && hasLowerRounds) {
1209
- // two to one relation
1210
- if (!lastLowerRound || !currentLowerRound || !previousUpperRound)
1211
- throw new Error('lastLowerRound or currentLowerRound or previousUpperRound is null');
1212
- // in a sync double elimination bracket, the final round has the same index as the last lower round
1213
- // in an async one, there is always one more round in the lower bracket since we only display a section of the whole tournament
1214
- const isAsyncBracket = currentLowerRound.id !== lastLowerRound.id;
1215
- const finalLowerRound = isAsyncBracket ? nextLowerRound : currentLowerRound;
1216
- if (!finalLowerRound)
1217
- throw new Error('finalLowerRound is null');
1218
- if (!firstLowerRound)
1219
- throw new Error('firstLowerRound is null');
1220
- if (finalLowerRound.id !== lastLowerRound.id)
1221
- throw new Error('finalLowerRound is not the last lower round');
1222
- // if we have a reverse final
1223
- if (nextUpperRound) {
1224
- // for the final
1225
- roundRelations.set(currentUpperRound.id, {
1226
- type: 'two-to-one',
1227
- previousUpperRound: previousUpperRound,
1228
- previousLowerRound: finalLowerRound,
1229
- nextRound: nextUpperRound,
1230
- currentRound: currentUpperRound,
1231
- nextRoundMatchFactor: nextUpperRound.matchCount / currentUpperRound.matchCount,
1232
- previousUpperRoundMatchFactor: previousUpperRound.matchCount / currentUpperRound.matchCount,
1233
- previousLowerRoundMatchFactor: finalLowerRound.matchCount / currentUpperRound.matchCount,
1234
- upperRootRoundMatchFactor: firstUpperRound.matchCount / currentUpperRound.matchCount,
1235
- lowerRootRoundMatchFactor: firstLowerRound.matchCount / currentUpperRound.matchCount,
1236
- });
1521
+ case 'single-elimination': {
1522
+ if (matchCount === 1) {
1523
+ return COMMON_BRACKET_ROUND_TYPE.FINAL;
1237
1524
  }
1238
1525
  else {
1239
- // no reverse final means the final is the last round
1240
- // for the final
1241
- roundRelations.set(currentUpperRound.id, {
1242
- type: 'two-to-nothing',
1243
- previousUpperRound: previousUpperRound,
1244
- previousLowerRound: finalLowerRound,
1245
- currentRound: currentUpperRound,
1246
- previousLowerRoundMatchFactor: finalLowerRound.matchCount / currentUpperRound.matchCount,
1247
- previousUpperRoundMatchFactor: previousUpperRound.matchCount / currentUpperRound.matchCount,
1248
- lowerRootRoundMatchFactor: firstLowerRound.matchCount / currentUpperRound.matchCount,
1249
- upperRootRoundMatchFactor: firstUpperRound.matchCount / currentUpperRound.matchCount,
1250
- });
1251
- }
1252
- if (isAsyncBracket) {
1253
- // if this is an async bracket, we need to set the relations for the 2 last lower rounds since they will be skipped by the default one to one logic
1254
- const preFinalLowerRound = lowerRounds[lowerRounds.length - 2] || null;
1255
- const prePreFinalLowerRound = lowerRounds[lowerRounds.length - 3] || null;
1256
- if (!preFinalLowerRound)
1257
- throw new Error('preFinalLowerRound is null');
1258
- if (!firstLowerRound)
1259
- throw new Error('firstLowerRound is null');
1260
- // for the last lower round
1261
- roundRelations.set(finalLowerRound.id, {
1262
- type: 'one-to-one',
1263
- previousRound: preFinalLowerRound,
1264
- nextRound: currentUpperRound,
1265
- currentRound: finalLowerRound,
1266
- nextRoundMatchFactor: currentUpperRound.matchCount / finalLowerRound.matchCount,
1267
- previousRoundMatchFactor: preFinalLowerRound.matchCount / finalLowerRound.matchCount,
1268
- rootRoundMatchFactor: firstLowerRound.matchCount / finalLowerRound.matchCount,
1269
- });
1270
- if (prePreFinalLowerRound) {
1271
- // for the pre final lower round
1272
- roundRelations.set(preFinalLowerRound.id, {
1273
- type: 'one-to-one',
1274
- previousRound: prePreFinalLowerRound,
1275
- nextRound: finalLowerRound,
1276
- currentRound: preFinalLowerRound,
1277
- nextRoundMatchFactor: finalLowerRound.matchCount / preFinalLowerRound.matchCount,
1278
- previousRoundMatchFactor: prePreFinalLowerRound.matchCount / preFinalLowerRound.matchCount,
1279
- rootRoundMatchFactor: firstLowerRound.matchCount / preFinalLowerRound.matchCount,
1280
- });
1281
- }
1282
- else {
1283
- // for the first lower round
1284
- roundRelations.set(preFinalLowerRound.id, {
1285
- type: 'nothing-to-one',
1286
- nextRound: finalLowerRound,
1287
- currentRound: preFinalLowerRound,
1288
- nextRoundMatchFactor: finalLowerRound.matchCount / preFinalLowerRound.matchCount,
1289
- });
1290
- }
1291
- }
1292
- else {
1293
- // this is a sync bracket, we only need to set the relation for the last lower round
1294
- if (!previousLowerRound)
1295
- throw new Error('previousLowerRound is null');
1296
- if (!firstLowerRound)
1297
- throw new Error('firstLowerRound is null');
1298
- // for the last lower round
1299
- roundRelations.set(finalLowerRound.id, {
1300
- type: 'one-to-one',
1301
- previousRound: previousLowerRound,
1302
- nextRound: currentUpperRound,
1303
- currentRound: finalLowerRound,
1304
- nextRoundMatchFactor: currentUpperRound.matchCount / finalLowerRound.matchCount,
1305
- previousRoundMatchFactor: previousLowerRound.matchCount / finalLowerRound.matchCount,
1306
- rootRoundMatchFactor: firstLowerRound.matchCount / finalLowerRound.matchCount,
1307
- });
1308
- }
1309
- }
1310
- else if (isLastUpperRound) {
1311
- // one to nothing relation
1312
- if (!previousUpperRound)
1313
- throw new Error('previousUpperRound is null');
1314
- roundRelations.set(currentUpperRound.id, {
1315
- type: 'one-to-nothing',
1316
- previousRound: previousUpperRound,
1317
- currentRound: currentUpperRound,
1318
- previousRoundMatchFactor: previousUpperRound.matchCount / currentUpperRound.matchCount,
1319
- rootRoundMatchFactor: firstUpperRound.matchCount / currentUpperRound.matchCount,
1320
- });
1321
- }
1322
- else if (currentUpperRound.isFirstRound) {
1323
- // nothing to one relation
1324
- if (!nextUpperRound)
1325
- throw new Error('nextUpperRound is null');
1326
- roundRelations.set(currentUpperRound.id, {
1327
- type: 'nothing-to-one',
1328
- nextRound: nextUpperRound,
1329
- currentRound: currentUpperRound,
1330
- nextRoundMatchFactor: nextUpperRound.matchCount / currentUpperRound.matchCount,
1331
- });
1332
- if (currentLowerRound) {
1333
- if (!nextLowerRound)
1334
- throw new Error('nextLowerRound is null');
1335
- roundRelations.set(currentLowerRound.id, {
1336
- type: 'nothing-to-one',
1337
- nextRound: nextLowerRound,
1338
- currentRound: currentLowerRound,
1339
- nextRoundMatchFactor: nextLowerRound.matchCount / currentLowerRound.matchCount,
1340
- });
1526
+ return SINGLE_ELIMINATION_BRACKET_ROUND_TYPE.SINGLE_ELIMINATION_BRACKET;
1341
1527
  }
1342
1528
  }
1343
- else {
1344
- // one to one relation
1345
- if (!previousUpperRound)
1346
- throw new Error('previousUpperRound is null');
1347
- if (!nextUpperRound)
1348
- throw new Error('nextUpperRound is null');
1349
- roundRelations.set(currentUpperRound.id, {
1350
- type: 'one-to-one',
1351
- previousRound: previousUpperRound,
1352
- nextRound: nextUpperRound,
1353
- currentRound: currentUpperRound,
1354
- nextRoundMatchFactor: nextUpperRound.matchCount / currentUpperRound.matchCount,
1355
- previousRoundMatchFactor: previousUpperRound.matchCount / currentUpperRound.matchCount,
1356
- rootRoundMatchFactor: firstUpperRound.matchCount / currentUpperRound.matchCount,
1357
- });
1358
- // we only want to set lower rounds here until the special merging point of the final.
1359
- // lower bracket rounds after and including the final will be set in the final round block
1360
- if (currentLowerRound && currentUpperRound.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET) {
1361
- if (!previousLowerRound)
1362
- throw new Error('previousLowerRound is null');
1363
- if (!nextLowerRound)
1364
- throw new Error('nextLowerRound is null');
1365
- if (!firstLowerRound)
1366
- throw new Error('firstLowerRound is null');
1367
- roundRelations.set(currentLowerRound.id, {
1368
- type: 'one-to-one',
1369
- previousRound: previousLowerRound,
1370
- nextRound: nextLowerRound,
1371
- currentRound: currentLowerRound,
1372
- nextRoundMatchFactor: nextLowerRound.matchCount / currentLowerRound.matchCount,
1373
- previousRoundMatchFactor: previousLowerRound.matchCount / currentLowerRound.matchCount,
1374
- rootRoundMatchFactor: firstLowerRound.matchCount / currentLowerRound.matchCount,
1375
- });
1376
- }
1529
+ default: {
1530
+ throw new Error(`Unsupported tournament mode: ${tournamentMode}`);
1377
1531
  }
1378
1532
  }
1379
- return roundRelations;
1380
1533
  };
1381
- const generateMatchRelations = (bracketData, roundRelations, matchPositionMaps) => {
1382
- const matchRelations = new Map();
1383
- for (const match of bracketData.matches.values()) {
1384
- const currentRelation = roundRelations.get(match.round.id);
1385
- if (!currentRelation)
1386
- throw new Error('Match round not found');
1387
- const { nextRoundMatchPosition, previousLowerRoundMatchPosition, previousUpperRoundMatchPosition } = generateMatchRelationPositions(currentRelation, match);
1388
- switch (currentRelation.type) {
1389
- case 'nothing-to-one': {
1390
- const nextMatch = matchPositionMaps.get(currentRelation.nextRound.id)?.get(nextRoundMatchPosition);
1391
- if (!nextMatch)
1392
- throw new Error('Next round match not found');
1393
- // means left is nothing. right is one
1394
- matchRelations.set(match.id, {
1395
- type: 'nothing-to-one',
1396
- currentMatch: match,
1397
- currentRound: currentRelation.currentRound,
1398
- nextRound: currentRelation.nextRound,
1399
- nextMatch,
1400
- });
1401
- break;
1402
- }
1403
- case 'one-to-nothing': {
1404
- const previousUpperMatch = matchPositionMaps
1405
- .get(currentRelation.previousRound.id)
1406
- ?.get(previousUpperRoundMatchPosition);
1407
- const previousLowerMatch = matchPositionMaps
1408
- .get(currentRelation.previousRound.id)
1409
- ?.get(previousLowerRoundMatchPosition);
1410
- if (!previousUpperMatch)
1411
- throw new Error('Previous round match not found');
1412
- if (previousUpperRoundMatchPosition !== previousLowerRoundMatchPosition) {
1413
- // means left is two. right is one
1414
- if (!previousLowerMatch)
1415
- throw new Error('Previous lower round match not found');
1416
- matchRelations.set(match.id, {
1417
- type: 'two-to-nothing',
1418
- currentMatch: match,
1419
- currentRound: currentRelation.currentRound,
1420
- previousUpperMatch,
1421
- previousUpperRound: currentRelation.previousRound,
1422
- previousLowerMatch,
1423
- previousLowerRound: currentRelation.previousRound,
1424
- });
1425
- }
1426
- else {
1427
- // means left is one. right is nothing
1428
- matchRelations.set(match.id, {
1429
- type: 'one-to-nothing',
1430
- currentMatch: match,
1431
- currentRound: currentRelation.currentRound,
1432
- previousMatch: previousUpperMatch,
1433
- previousRound: currentRelation.previousRound,
1434
- });
1435
- }
1436
- break;
1437
- }
1438
- case 'one-to-one': {
1439
- const nextMatch = matchPositionMaps.get(currentRelation.nextRound.id)?.get(nextRoundMatchPosition);
1440
- const previousUpperMatch = matchPositionMaps
1441
- .get(currentRelation.previousRound.id)
1442
- ?.get(previousUpperRoundMatchPosition);
1443
- const previousLowerMatch = matchPositionMaps
1444
- .get(currentRelation.previousRound.id)
1445
- ?.get(previousLowerRoundMatchPosition);
1446
- if (!nextMatch)
1447
- throw new Error('Next round match not found');
1448
- if (!previousUpperMatch)
1449
- throw new Error(`Previous upper round match not found`);
1450
- if (!previousLowerMatch)
1451
- throw new Error('Previous lower round match not found');
1452
- // can be either one to one or two to one
1453
- const isLeftOne = previousUpperRoundMatchPosition === previousLowerRoundMatchPosition;
1454
- if (isLeftOne) {
1455
- // one-to-one
1456
- matchRelations.set(match.id, {
1457
- type: 'one-to-one',
1458
- currentMatch: match,
1459
- currentRound: currentRelation.currentRound,
1460
- previousMatch: previousUpperMatch,
1461
- previousRound: currentRelation.previousRound,
1462
- nextMatch,
1463
- nextRound: currentRelation.nextRound,
1464
- });
1465
- }
1466
- else {
1467
- // two-to-one
1468
- matchRelations.set(match.id, {
1469
- type: 'two-to-one',
1470
- currentMatch: match,
1471
- currentRound: currentRelation.currentRound,
1472
- previousUpperMatch,
1473
- previousUpperRound: currentRelation.previousRound,
1474
- previousLowerMatch,
1475
- previousLowerRound: currentRelation.previousRound,
1476
- nextMatch,
1477
- nextRound: currentRelation.nextRound,
1478
- });
1479
- }
1480
- break;
1481
- }
1482
- case 'two-to-one': {
1483
- const nextMatch = matchPositionMaps.get(currentRelation.nextRound.id)?.get(nextRoundMatchPosition);
1484
- const previousUpperMatch = matchPositionMaps
1485
- .get(currentRelation.previousUpperRound.id)
1486
- ?.get(previousUpperRoundMatchPosition);
1487
- const previousLowerMatch = matchPositionMaps
1488
- .get(currentRelation.previousLowerRound.id)
1489
- ?.get(previousLowerRoundMatchPosition);
1490
- if (!nextMatch)
1491
- throw new Error('Next round match not found');
1492
- if (!previousUpperMatch)
1493
- throw new Error(`Previous upper round match not found`);
1494
- if (!previousLowerMatch)
1495
- throw new Error('Previous lower round match not found');
1496
- matchRelations.set(match.id, {
1497
- type: 'two-to-one',
1498
- currentMatch: match,
1499
- currentRound: currentRelation.currentRound,
1500
- previousUpperMatch,
1501
- previousUpperRound: currentRelation.previousUpperRound,
1502
- previousLowerMatch,
1503
- previousLowerRound: currentRelation.previousLowerRound,
1504
- nextMatch,
1505
- nextRound: currentRelation.nextRound,
1506
- });
1507
- break;
1508
- }
1509
- case 'two-to-nothing': {
1510
- const previousUpperMatch = matchPositionMaps
1511
- .get(currentRelation.previousUpperRound.id)
1512
- ?.get(previousUpperRoundMatchPosition);
1513
- const previousLowerMatch = matchPositionMaps
1514
- .get(currentRelation.previousUpperRound.id)
1515
- ?.get(previousLowerRoundMatchPosition);
1516
- if (!previousUpperMatch)
1517
- throw new Error(`Previous upper round match not found`);
1518
- if (!previousLowerMatch)
1519
- throw new Error('Previous lower round match not found');
1520
- matchRelations.set(match.id, {
1521
- type: 'two-to-nothing',
1522
- currentMatch: match,
1523
- currentRound: currentRelation.currentRound,
1524
- previousUpperMatch,
1525
- previousUpperRound: currentRelation.previousUpperRound,
1526
- previousLowerMatch,
1527
- previousLowerRound: currentRelation.previousUpperRound,
1528
- });
1529
- break;
1530
- }
1534
+ const generateBracketDataForGg = (source) => {
1535
+ const tournamentMode = generateTournamentModeFormGgData(source);
1536
+ const bracketData = {
1537
+ rounds: [],
1538
+ matches: [],
1539
+ mode: tournamentMode,
1540
+ };
1541
+ const matchesGrouped = new Map();
1542
+ for (const match of source.matches) {
1543
+ const roundId = `${match.roundNumber}-${match.stageNumber}-${match.bracketType || null}`;
1544
+ if (!matchesGrouped.has(roundId)) {
1545
+ matchesGrouped.set(roundId, []);
1531
1546
  }
1547
+ matchesGrouped.get(roundId)?.push(match);
1532
1548
  }
1533
- return matchRelations;
1549
+ for (const [roundId, matches] of matchesGrouped.entries()) {
1550
+ const firstMatch = matches[0];
1551
+ if (!firstMatch)
1552
+ throw new Error('First match not found');
1553
+ const roundType = generateRoundTypeFromGgMatch(tournamentMode, firstMatch.bracketType, firstMatch.stageNumber, matches.length);
1554
+ const bracketRound = {
1555
+ type: roundType,
1556
+ id: roundId,
1557
+ data: null,
1558
+ name: firstMatch.roundTitle,
1559
+ };
1560
+ bracketData.rounds.push(bracketRound);
1561
+ for (const match of matches) {
1562
+ const bracketMatch = {
1563
+ id: match.id,
1564
+ data: match,
1565
+ roundId: roundId,
1566
+ home: match.homeMatchSide.participant?.id || null,
1567
+ away: match.awayMatchSide.participant?.id || null,
1568
+ winner: match.winningSide,
1569
+ status: match.status === 'completed' ? 'completed' : 'pending',
1570
+ };
1571
+ bracketData.matches.push(bracketMatch);
1572
+ }
1573
+ }
1574
+ return bracketData;
1534
1575
  };
1535
- const generateMatchRelationPositions = (currentRelation, match) => {
1536
- switch (currentRelation.type) {
1537
- case 'nothing-to-one': {
1576
+
1577
+ class NewBracketDefaultMatchComponent {
1578
+ constructor() {
1579
+ this.bracketRound = input.required();
1580
+ this.bracketMatch = input.required();
1581
+ }
1582
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultMatchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1583
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", type: NewBracketDefaultMatchComponent, isStandalone: true, selector: "et-new-bracket-default-match", inputs: { bracketRound: { classPropertyName: "bracketRound", publicName: "bracketRound", isSignal: true, isRequired: true, transformFunction: null }, bracketMatch: { classPropertyName: "bracketMatch", publicName: "bracketMatch", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-new-bracket-default-match-host" }, ngImport: i0, template: ` {{ bracketMatch().id }} `, isInline: true, styles: [".et-new-bracket-default-match-host{display:block;padding:8px;border:1px solid yellow;inline-size:250px;block-size:75px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;font-size:12px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
1584
+ }
1585
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultMatchComponent, decorators: [{
1586
+ type: Component,
1587
+ args: [{ selector: 'et-new-bracket-default-match', template: ` {{ bracketMatch().id }} `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
1588
+ class: 'et-new-bracket-default-match-host',
1589
+ }, styles: [".et-new-bracket-default-match-host{display:block;padding:8px;border:1px solid yellow;inline-size:250px;block-size:75px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;font-size:12px}\n"] }]
1590
+ }] });
1591
+
1592
+ class NewBracketDefaultRoundHeaderComponent {
1593
+ constructor() {
1594
+ this.bracketRound = input.required();
1595
+ }
1596
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultRoundHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1597
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", type: NewBracketDefaultRoundHeaderComponent, isStandalone: true, selector: "et-new-bracket-default-round-header", inputs: { bracketRound: { classPropertyName: "bracketRound", publicName: "bracketRound", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-new-bracket-default-round-header-host" }, ngImport: i0, template: ` {{ bracketRound().name }} `, isInline: true, styles: [".et-new-bracket-default-round-header-host{display:block;padding:8px;border:1px solid green;inline-size:250px;block-size:50px;display:flex;justify-content:center;align-items:center;box-sizing:border-box}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
1598
+ }
1599
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultRoundHeaderComponent, decorators: [{
1600
+ type: Component,
1601
+ args: [{ selector: 'et-new-bracket-default-round-header', template: ` {{ bracketRound().name }} `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
1602
+ class: 'et-new-bracket-default-round-header-host',
1603
+ }, styles: [".et-new-bracket-default-round-header-host{display:block;padding:8px;border:1px solid green;inline-size:250px;block-size:50px;display:flex;justify-content:center;align-items:center;box-sizing:border-box}\n"] }]
1604
+ }] });
1605
+
1606
+ const FALLBACK_MATCH_RELATION_POSITION = -1;
1607
+ const generateMatchPosition = (match, factor) => Math.ceil(match.position * factor);
1608
+ const generateMatchRelationPositions = (relation, match) => {
1609
+ switch (relation.type) {
1610
+ case 'nothing-to-one':
1538
1611
  return {
1539
- nextRoundMatchPosition: generateMatchPosition(match, currentRelation.nextRoundMatchFactor),
1540
- previousUpperRoundMatchPosition: FALLBACK_MATCH_POSITION,
1541
- previousLowerRoundMatchPosition: FALLBACK_MATCH_POSITION,
1612
+ nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
1613
+ previousUpperRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
1614
+ previousLowerRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
1542
1615
  };
1543
- }
1544
1616
  case 'one-to-nothing': {
1545
- const previousRoundHasDoubleTheMatchCount = currentRelation.previousRoundMatchFactor === 2;
1546
- const doubleUpperMatchCountShift = previousRoundHasDoubleTheMatchCount ? 1 : 0;
1617
+ const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
1547
1618
  return {
1548
- nextRoundMatchPosition: FALLBACK_MATCH_POSITION,
1549
- previousUpperRoundMatchPosition: (generateMatchPosition(match, currentRelation.previousRoundMatchFactor) -
1550
- doubleUpperMatchCountShift),
1551
- previousLowerRoundMatchPosition: generateMatchPosition(match, currentRelation.previousRoundMatchFactor),
1619
+ nextRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
1620
+ previousUpperRoundMatchPosition: (generateMatchPosition(match, relation.previousRoundMatchFactor) -
1621
+ double),
1622
+ previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
1552
1623
  };
1553
1624
  }
1554
1625
  case 'one-to-one': {
1555
- const previousRoundHasDoubleTheMatchCount = currentRelation.previousRoundMatchFactor === 2;
1556
- const doubleUpperMatchCountShift = previousRoundHasDoubleTheMatchCount ? 1 : 0;
1626
+ const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
1557
1627
  return {
1558
- nextRoundMatchPosition: generateMatchPosition(match, currentRelation.nextRoundMatchFactor),
1559
- previousUpperRoundMatchPosition: (generateMatchPosition(match, currentRelation.previousRoundMatchFactor) -
1560
- doubleUpperMatchCountShift),
1561
- previousLowerRoundMatchPosition: generateMatchPosition(match, currentRelation.previousRoundMatchFactor),
1628
+ nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
1629
+ previousUpperRoundMatchPosition: (generateMatchPosition(match, relation.previousRoundMatchFactor) -
1630
+ double),
1631
+ previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
1562
1632
  };
1563
1633
  }
1564
- case 'two-to-one': {
1634
+ case 'two-to-one':
1565
1635
  return {
1566
- nextRoundMatchPosition: generateMatchPosition(match, currentRelation.nextRoundMatchFactor),
1567
- previousUpperRoundMatchPosition: generateMatchPosition(match, currentRelation.previousUpperRoundMatchFactor),
1568
- previousLowerRoundMatchPosition: generateMatchPosition(match, currentRelation.previousLowerRoundMatchFactor),
1636
+ nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
1637
+ previousUpperRoundMatchPosition: generateMatchPosition(match, relation.previousUpperRoundMatchFactor),
1638
+ previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousLowerRoundMatchFactor),
1569
1639
  };
1570
- }
1571
- case 'two-to-nothing': {
1640
+ case 'two-to-nothing':
1572
1641
  return {
1573
- nextRoundMatchPosition: FALLBACK_MATCH_POSITION,
1574
- previousUpperRoundMatchPosition: generateMatchPosition(match, currentRelation.previousUpperRoundMatchFactor),
1575
- previousLowerRoundMatchPosition: generateMatchPosition(match, currentRelation.previousLowerRoundMatchFactor),
1642
+ nextRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
1643
+ previousUpperRoundMatchPosition: generateMatchPosition(match, relation.previousUpperRoundMatchFactor),
1644
+ previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousLowerRoundMatchFactor),
1576
1645
  };
1577
- }
1578
1646
  }
1579
1647
  };
1580
- const generateMatchPosition = (match, factor) => {
1581
- return Math.ceil(match.position * factor);
1648
+ const createNothingToOneRelation$1 = (params) => {
1649
+ const { match, relation, matchPositionMaps, nextRoundMatchPosition } = params;
1650
+ const nextMatch = matchPositionMaps.get(relation.nextRound.id)?.get(nextRoundMatchPosition);
1651
+ if (!nextMatch)
1652
+ throw new Error('Next round match not found');
1653
+ return {
1654
+ type: 'nothing-to-one',
1655
+ currentMatch: match,
1656
+ currentRound: relation.currentRound,
1657
+ nextMatch,
1658
+ nextRound: relation.nextRound,
1659
+ };
1582
1660
  };
1583
- const logRoundRelations = (roundRelations, bracketData) => {
1584
- for (const [roundId, relation] of roundRelations.entries()) {
1585
- const round = bracketData.rounds.get(roundId);
1586
- if (!round) {
1587
- console.error(`Round with id ${roundId} not found in bracket data. The bracket will be malformed.`);
1588
- continue;
1589
- }
1661
+ const createOneToNothingOrTwoToNothingRelation = (params) => {
1662
+ const { match, relation, matchPositionMaps, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition } = params;
1663
+ const previousUpperMatch = matchPositionMaps.get(relation.previousRound.id)?.get(previousUpperRoundMatchPosition);
1664
+ const previousLowerMatch = matchPositionMaps.get(relation.previousRound.id)?.get(previousLowerRoundMatchPosition);
1665
+ if (!previousUpperMatch)
1666
+ throw new Error('Previous round match not found');
1667
+ if (previousUpperRoundMatchPosition !== previousLowerRoundMatchPosition) {
1668
+ if (!previousLowerMatch)
1669
+ throw new Error('Previous lower round match not found');
1670
+ return {
1671
+ type: 'two-to-nothing',
1672
+ currentMatch: match,
1673
+ currentRound: relation.currentRound,
1674
+ previousUpperMatch,
1675
+ previousUpperRound: relation.previousRound,
1676
+ previousLowerMatch,
1677
+ previousLowerRound: relation.previousRound,
1678
+ };
1679
+ }
1680
+ else {
1681
+ return {
1682
+ type: 'one-to-nothing',
1683
+ currentMatch: match,
1684
+ currentRound: relation.currentRound,
1685
+ previousMatch: previousUpperMatch,
1686
+ previousRound: relation.previousRound,
1687
+ };
1688
+ }
1689
+ };
1690
+ const createOneToOneOrTwoToOneRelation = (params) => {
1691
+ const { match, relation, matchPositionMaps, nextRoundMatchPosition, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition, } = params;
1692
+ const nextMatch = matchPositionMaps.get(relation.nextRound.id)?.get(nextRoundMatchPosition);
1693
+ const previousUpperMatch = matchPositionMaps.get(relation.previousRound.id)?.get(previousUpperRoundMatchPosition);
1694
+ const previousLowerMatch = matchPositionMaps.get(relation.previousRound.id)?.get(previousLowerRoundMatchPosition);
1695
+ if (!nextMatch)
1696
+ throw new Error('Next round match not found');
1697
+ if (!previousUpperMatch)
1698
+ throw new Error('Previous upper round match not found');
1699
+ if (!previousLowerMatch)
1700
+ throw new Error('Previous lower round match not found');
1701
+ if (previousUpperRoundMatchPosition === previousLowerRoundMatchPosition) {
1702
+ return {
1703
+ type: 'one-to-one',
1704
+ currentMatch: match,
1705
+ currentRound: relation.currentRound,
1706
+ previousMatch: previousUpperMatch,
1707
+ previousRound: relation.previousRound,
1708
+ nextMatch,
1709
+ nextRound: relation.nextRound,
1710
+ };
1711
+ }
1712
+ else {
1713
+ return {
1714
+ type: 'two-to-one',
1715
+ currentMatch: match,
1716
+ currentRound: relation.currentRound,
1717
+ previousUpperMatch,
1718
+ previousUpperRound: relation.previousRound,
1719
+ previousLowerMatch,
1720
+ previousLowerRound: relation.previousRound,
1721
+ nextMatch,
1722
+ nextRound: relation.nextRound,
1723
+ };
1724
+ }
1725
+ };
1726
+ const createTwoToOneRelation$1 = (params) => {
1727
+ const { match, relation, matchPositionMaps, nextRoundMatchPosition, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition, } = params;
1728
+ const nextMatch = matchPositionMaps.get(relation.nextRound.id)?.get(nextRoundMatchPosition);
1729
+ const previousUpperMatch = matchPositionMaps
1730
+ .get(relation.previousUpperRound.id)
1731
+ ?.get(previousUpperRoundMatchPosition);
1732
+ const previousLowerMatch = matchPositionMaps
1733
+ .get(relation.previousLowerRound.id)
1734
+ ?.get(previousLowerRoundMatchPosition);
1735
+ if (!nextMatch)
1736
+ throw new Error('Next round match not found');
1737
+ if (!previousUpperMatch)
1738
+ throw new Error('Previous upper round match not found');
1739
+ if (!previousLowerMatch)
1740
+ throw new Error('Previous lower round match not found');
1741
+ return {
1742
+ type: 'two-to-one',
1743
+ currentMatch: match,
1744
+ currentRound: relation.currentRound,
1745
+ previousUpperMatch,
1746
+ previousUpperRound: relation.previousUpperRound,
1747
+ previousLowerMatch,
1748
+ previousLowerRound: relation.previousLowerRound,
1749
+ nextMatch,
1750
+ nextRound: relation.nextRound,
1751
+ };
1752
+ };
1753
+ const createTwoToNothingRelation$1 = (params) => {
1754
+ const { match, relation, matchPositionMaps, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition } = params;
1755
+ const previousUpperMatch = matchPositionMaps
1756
+ .get(relation.previousUpperRound.id)
1757
+ ?.get(previousUpperRoundMatchPosition);
1758
+ const previousLowerMatch = matchPositionMaps
1759
+ .get(relation.previousUpperRound.id)
1760
+ ?.get(previousLowerRoundMatchPosition);
1761
+ if (!previousUpperMatch)
1762
+ throw new Error('Previous upper round match not found');
1763
+ if (!previousLowerMatch)
1764
+ throw new Error('Previous lower round match not found');
1765
+ return {
1766
+ type: 'two-to-nothing',
1767
+ currentMatch: match,
1768
+ currentRound: relation.currentRound,
1769
+ previousUpperMatch,
1770
+ previousUpperRound: relation.previousUpperRound,
1771
+ previousLowerMatch,
1772
+ previousLowerRound: relation.previousUpperRound,
1773
+ };
1774
+ };
1775
+ const generateMatchRelationsNew = (bracketData) => {
1776
+ const matchRelations = [];
1777
+ const matchPositionMaps = new Map();
1778
+ for (const round of bracketData.rounds.values()) {
1779
+ const matchMap = new Map([...round.matches.values()].map((m) => [m.position, m]));
1780
+ matchPositionMaps.set(round.id, matchMap);
1781
+ }
1782
+ for (const match of bracketData.matches.values()) {
1783
+ const relation = match.round.relation;
1784
+ const { nextRoundMatchPosition, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition } = generateMatchRelationPositions(relation, match);
1590
1785
  switch (relation.type) {
1591
1786
  case 'nothing-to-one':
1592
- console.log(`START: ${round.name} -> ${relation.nextRound.name} (F: ${relation.nextRoundMatchFactor})`);
1787
+ matchRelations.push(createNothingToOneRelation$1({
1788
+ match,
1789
+ relation,
1790
+ matchPositionMaps,
1791
+ nextRoundMatchPosition,
1792
+ }));
1593
1793
  break;
1594
1794
  case 'one-to-nothing':
1595
- console.log(`${relation.previousRound.name} (F: ${relation.previousRoundMatchFactor}) <- ENDING: ${round.name}`);
1795
+ matchRelations.push(createOneToNothingOrTwoToNothingRelation({
1796
+ match,
1797
+ relation,
1798
+ matchPositionMaps,
1799
+ previousUpperRoundMatchPosition,
1800
+ previousLowerRoundMatchPosition,
1801
+ }));
1596
1802
  break;
1597
1803
  case 'one-to-one':
1598
- console.log(`${relation.previousRound.name} (F: ${relation.previousRoundMatchFactor}) <- ${round.name} -> ${relation.nextRound.name} (F: ${relation.nextRoundMatchFactor})`);
1804
+ matchRelations.push(createOneToOneOrTwoToOneRelation({
1805
+ match,
1806
+ relation,
1807
+ matchPositionMaps,
1808
+ nextRoundMatchPosition,
1809
+ previousUpperRoundMatchPosition,
1810
+ previousLowerRoundMatchPosition,
1811
+ }));
1599
1812
  break;
1600
1813
  case 'two-to-one':
1601
- console.log(`MERGER: ${relation.previousUpperRound.name} (F: ${relation.previousUpperRoundMatchFactor}) AND ${relation.previousLowerRound.name} (F: ${relation.previousLowerRoundMatchFactor}) <- ${round.name} -> ${relation.nextRound.name} (F: ${relation.nextRoundMatchFactor})`);
1814
+ matchRelations.push(createTwoToOneRelation$1({
1815
+ match,
1816
+ relation,
1817
+ matchPositionMaps,
1818
+ nextRoundMatchPosition,
1819
+ previousUpperRoundMatchPosition,
1820
+ previousLowerRoundMatchPosition,
1821
+ }));
1602
1822
  break;
1603
1823
  case 'two-to-nothing':
1604
- console.log(`MERGER: ${relation.previousUpperRound.name} (F: ${relation.previousUpperRoundMatchFactor}) AND ${relation.previousLowerRound.name} (F: ${relation.previousLowerRoundMatchFactor}) <- ENDING: ${round.name}`);
1824
+ matchRelations.push(createTwoToNothingRelation$1({
1825
+ match,
1826
+ relation,
1827
+ matchPositionMaps,
1828
+ previousUpperRoundMatchPosition,
1829
+ previousLowerRoundMatchPosition,
1830
+ }));
1605
1831
  break;
1606
1832
  }
1607
1833
  }
1834
+ return matchRelations;
1608
1835
  };
1609
- const generateMatchParticipantMap = (bracketData) => {
1610
- const matchParticipantMap = new Map();
1611
- const hasElimination = bracketData.mode === TOURNAMENT_MODE.SINGLE_ELIMINATION ||
1612
- bracketData.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION ||
1613
- bracketData.mode === TOURNAMENT_MODE.SWISS_WITH_ELIMINATION;
1614
- for (const participant of bracketData.participants.values()) {
1615
- let winsTilNow = 0;
1616
- let lossesTilNow = 0;
1617
- let tiesTilNow = 0;
1618
- for (const matchParticipantMatch of participant.matches.values()) {
1619
- const isWinner = matchParticipantMatch.bracketMatch.winnerSide === matchParticipantMatch.side;
1620
- const isLooser = matchParticipantMatch.bracketMatch.winnerSide &&
1621
- matchParticipantMatch.bracketMatch.winnerSide !== matchParticipantMatch.side;
1622
- const isTie = matchParticipantMatch.bracketMatch.status === 'completed' && !matchParticipantMatch.bracketMatch.winner;
1623
- if (isWinner) {
1624
- winsTilNow++;
1625
- }
1626
- else if (isLooser) {
1627
- lossesTilNow++;
1628
- }
1629
- else if (isTie) {
1630
- tiesTilNow++;
1631
- }
1632
- let isEliminated = false;
1633
- let isEliminationMatch = false;
1634
- if (hasElimination) {
1635
- // TODO: Implement elimination logic
1636
- // Means the current match is loss and it's the last match of the participant
1637
- isEliminated = false;
1638
- // Always true for single elimination, never for e.g. groups, depends on the round for double elimination, depends on the loss count for swiss with elimination
1639
- isEliminationMatch = false;
1640
- }
1641
- // TODO: Implement round logic
1642
- // true if its the first round of the bracket
1643
- const isFirstRound = false;
1644
- // true if its the last round of the bracket (eg. final for single elimination)
1645
- const isLastRound = false;
1646
- const participantMatchData = {
1647
- bracketMatch: matchParticipantMatch.bracketMatch,
1648
- result: isWinner ? 'win' : isLooser ? 'loss' : isTie ? 'tie' : null,
1649
- isEliminated,
1650
- isEliminationMatch,
1651
- isFirstRound,
1652
- isLastRound,
1653
- tieCount: tiesTilNow,
1654
- winCount: winsTilNow,
1655
- lossCount: lossesTilNow,
1656
- };
1657
- if (!matchParticipantMap.has(participant.id)) {
1658
- matchParticipantMap.set(participant.id, {
1659
- id: participant.id,
1660
- name: participant.name,
1661
- matches: new Map(),
1662
- });
1663
- }
1664
- const participantData = matchParticipantMap.get(participant.id);
1665
- participantData.matches.set(matchParticipantMatch.bracketMatch.id, participantMatchData);
1666
- }
1667
- }
1668
- return matchParticipantMap;
1669
- };
1670
- const factorialCache = new Map();
1671
- const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
1672
- const ADVANCE_WINS = 3;
1673
- const ELIMINATE_LOSSES = 3;
1674
- // Cache factorial calculations
1675
- const getFactorial = (n) => {
1676
- if (n <= 1)
1677
- return 1;
1678
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1679
- if (factorialCache.has(n))
1680
- return factorialCache.get(n);
1681
- const result = n * getFactorial(n - 1);
1682
- factorialCache.set(n, result);
1683
- return result;
1836
+
1837
+ const calculateMatchFactor = (numeratorRound, denominatorRound) => numeratorRound.matchCount / denominatorRound.matchCount;
1838
+ const createNothingToOneRelation = (params) => ({
1839
+ type: 'nothing-to-one',
1840
+ currentRound: params.currentRound,
1841
+ nextRound: params.nextRound,
1842
+ nextRoundMatchFactor: calculateMatchFactor(params.nextRound, params.currentRound),
1843
+ });
1844
+ const createOneToNothingRelation = (params) => ({
1845
+ type: 'one-to-nothing',
1846
+ currentRound: params.currentRound,
1847
+ previousRound: params.previousRound,
1848
+ previousRoundMatchFactor: calculateMatchFactor(params.previousRound, params.currentRound),
1849
+ rootRoundMatchFactor: calculateMatchFactor(params.rootRound, params.currentRound),
1850
+ });
1851
+ const createOneToOneRelation = (params) => ({
1852
+ type: 'one-to-one',
1853
+ currentRound: params.currentRound,
1854
+ previousRound: params.previousRound,
1855
+ nextRound: params.nextRound,
1856
+ nextRoundMatchFactor: calculateMatchFactor(params.nextRound, params.currentRound),
1857
+ previousRoundMatchFactor: calculateMatchFactor(params.previousRound, params.currentRound),
1858
+ rootRoundMatchFactor: calculateMatchFactor(params.rootRound, params.currentRound),
1859
+ });
1860
+ const createTwoToOneRelation = (params) => ({
1861
+ type: 'two-to-one',
1862
+ currentRound: params.currentRound,
1863
+ previousUpperRound: params.previousUpperRound,
1864
+ previousLowerRound: params.previousLowerRound,
1865
+ nextRound: params.nextRound,
1866
+ nextRoundMatchFactor: calculateMatchFactor(params.nextRound, params.currentRound),
1867
+ previousUpperRoundMatchFactor: calculateMatchFactor(params.previousUpperRound, params.currentRound),
1868
+ previousLowerRoundMatchFactor: calculateMatchFactor(params.previousLowerRound, params.currentRound),
1869
+ upperRootRoundMatchFactor: calculateMatchFactor(params.firstUpperRound, params.currentRound),
1870
+ lowerRootRoundMatchFactor: calculateMatchFactor(params.firstLowerRound, params.currentRound),
1871
+ });
1872
+ const createTwoToNothingRelation = (params) => ({
1873
+ type: 'two-to-nothing',
1874
+ currentRound: params.currentRound,
1875
+ previousUpperRound: params.previousUpperRound,
1876
+ previousLowerRound: params.previousLowerRound,
1877
+ previousUpperRoundMatchFactor: calculateMatchFactor(params.previousUpperRound, params.currentRound),
1878
+ previousLowerRoundMatchFactor: calculateMatchFactor(params.previousLowerRound, params.currentRound),
1879
+ upperRootRoundMatchFactor: calculateMatchFactor(params.firstUpperRound, params.currentRound),
1880
+ lowerRootRoundMatchFactor: calculateMatchFactor(params.firstLowerRound, params.currentRound),
1881
+ });
1882
+ const getNavigationContext = (params) => {
1883
+ const { upperRounds, currentUpperRoundIndex } = params;
1884
+ const currentUpperRound = upperRounds[currentUpperRoundIndex];
1885
+ if (!currentUpperRound)
1886
+ throw new Error('currentUpperRound is null');
1887
+ const isLeftToRight = !currentUpperRound.mirrorRoundType || currentUpperRound.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.LEFT;
1888
+ const relativePrevious = upperRounds[currentUpperRoundIndex - 1] || null;
1889
+ const relativeNext = upperRounds[currentUpperRoundIndex + 1] || null;
1890
+ const previousUpperRound = isLeftToRight ? relativePrevious : relativeNext;
1891
+ const nextUpperRound = isLeftToRight ? relativeNext : relativePrevious;
1892
+ const isLastUpperRound = !nextUpperRound ||
1893
+ (nextUpperRound.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.RIGHT && !currentUpperRound.mirrorRoundType);
1894
+ const isFinal = currentUpperRound.type === COMMON_BRACKET_ROUND_TYPE.FINAL;
1895
+ return {
1896
+ currentUpperRound,
1897
+ previousUpperRound,
1898
+ nextUpperRound,
1899
+ isLastUpperRound,
1900
+ isFinal,
1684
1901
  };
1685
- // Pre-calculate roundFactorial
1686
- const roundFact = getFactorial(roundNumber);
1687
- let totalCombinations = 0;
1688
- const validGroups = [];
1689
- // Single loop to gather valid groups and total combinations
1690
- for (let wins = roundNumber; wins >= 0; wins--) {
1691
- const losses = roundNumber - wins;
1692
- const remainingGames = ADVANCE_WINS + ELIMINATE_LOSSES - (wins + losses) - 1;
1693
- const notYetEliminated = losses < ELIMINATE_LOSSES;
1694
- const canStillAdvance = wins < ADVANCE_WINS && remainingGames >= 0;
1695
- if (!canStillAdvance || !notYetEliminated)
1696
- continue;
1697
- const combinations = roundFact / (getFactorial(wins) * getFactorial(losses));
1698
- totalCombinations += combinations;
1699
- validGroups.push({ wins, losses, combinations });
1700
- }
1701
- // Create final groups with calculated proportions
1702
- return validGroups.map(({ wins, losses, combinations }) => ({
1703
- id: `${wins}-${losses}`,
1704
- name: `${wins}-${losses}`,
1705
- matchesInGroup: Math.round((combinations / totalCombinations) * totalMatchesInRound),
1706
- }));
1707
1902
  };
1708
- const generateBracketRoundSwissGroupMaps = (bracketData, matchParticipantMap) => {
1709
- if (bracketData.mode !== TOURNAMENT_MODE.SWISS_WITH_ELIMINATION) {
1710
- return null;
1903
+ const handleFinalRound = (params) => {
1904
+ const { relations, currentUpperRound, previousUpperRound, nextUpperRound, lowerRounds, currentUpperRoundIndex, firstUpperRound, firstLowerRound, lastLowerRound, } = params;
1905
+ const currentLowerRound = lowerRounds[currentUpperRoundIndex] || null;
1906
+ const nextLowerRound = lowerRounds[currentUpperRoundIndex + 1] || null;
1907
+ const previousLowerRound = lowerRounds[currentUpperRoundIndex - 1] || null;
1908
+ if (!currentLowerRound)
1909
+ throw new Error('currentLowerRound is null');
1910
+ const isAsyncBracket = currentLowerRound.id !== lastLowerRound.id;
1911
+ const finalLowerRound = isAsyncBracket ? nextLowerRound : currentLowerRound;
1912
+ if (!finalLowerRound)
1913
+ throw new Error('finalLowerRound is null');
1914
+ if (finalLowerRound.id !== lastLowerRound.id)
1915
+ throw new Error('finalLowerRound is not the last lower round');
1916
+ if (nextUpperRound) {
1917
+ relations.push(createTwoToOneRelation({
1918
+ currentRound: currentUpperRound,
1919
+ previousUpperRound,
1920
+ previousLowerRound: finalLowerRound,
1921
+ nextRound: nextUpperRound,
1922
+ firstUpperRound,
1923
+ firstLowerRound,
1924
+ }));
1711
1925
  }
1712
- const roundsWithSwissGroups = new Map();
1713
- let roundNumber = 0;
1714
- for (const bracketRound of bracketData.rounds.values()) {
1715
- const availableGroups = getAvailableSwissGroupsForRound(roundNumber, bracketRound.matchCount);
1716
- const roundSwissData = {
1717
- groups: new Map(),
1718
- };
1719
- for (const group of availableGroups) {
1720
- const subGroup = {
1721
- id: group.id,
1722
- name: group.name,
1723
- matches: new Map(),
1724
- allowedMatchCount: group.matchesInGroup,
1725
- };
1726
- roundSwissData.groups.set(group.id, subGroup);
1727
- }
1728
- const emptyMatchIds = [];
1729
- for (const match of bracketRound.matches.values()) {
1730
- const participantHome = match.home ? (matchParticipantMap.get(match.home.id) ?? null) : null;
1731
- const participantAway = match.away ? (matchParticipantMap.get(match.away.id) ?? null) : null;
1732
- const anyParticipant = participantHome || participantAway;
1733
- if (!anyParticipant) {
1734
- emptyMatchIds.push(match.id);
1735
- continue;
1736
- }
1737
- const matchParticipantMatch = anyParticipant.matches.get(match.id);
1738
- if (!matchParticipantMatch)
1739
- throw new Error('Match participant match not found');
1740
- const wins = matchParticipantMatch.winCount;
1741
- const losses = matchParticipantMatch.lossCount;
1742
- const group = roundSwissData.groups.get(`${wins}-${losses}`);
1743
- if (!group)
1744
- throw new Error('Group not found for match: ' + match.id);
1745
- group.matches.set(match.id, match);
1926
+ else {
1927
+ relations.push(createTwoToNothingRelation({
1928
+ currentRound: currentUpperRound,
1929
+ previousUpperRound,
1930
+ previousLowerRound: finalLowerRound,
1931
+ firstUpperRound,
1932
+ firstLowerRound,
1933
+ }));
1934
+ }
1935
+ if (isAsyncBracket) {
1936
+ const preFinalLowerRound = lowerRounds[lowerRounds.length - 2];
1937
+ const prePreFinalLowerRound = lowerRounds[lowerRounds.length - 3] || null;
1938
+ if (!preFinalLowerRound)
1939
+ throw new Error('preFinalLowerRound is null');
1940
+ relations.push(createOneToOneRelation({
1941
+ currentRound: finalLowerRound,
1942
+ previousRound: preFinalLowerRound,
1943
+ nextRound: currentUpperRound,
1944
+ rootRound: firstLowerRound,
1945
+ }));
1946
+ if (prePreFinalLowerRound) {
1947
+ relations.push(createOneToOneRelation({
1948
+ currentRound: preFinalLowerRound,
1949
+ previousRound: prePreFinalLowerRound,
1950
+ nextRound: finalLowerRound,
1951
+ rootRound: firstLowerRound,
1952
+ }));
1746
1953
  }
1747
- for (const emptyMatchId of emptyMatchIds) {
1748
- const match = bracketRound.matches.get(emptyMatchId);
1749
- if (!match)
1750
- throw new Error('Empty match not found');
1751
- let groupFound = false;
1752
- for (const group of roundSwissData.groups.values()) {
1753
- if (group.matches.size < group.allowedMatchCount) {
1754
- group.matches.set(match.id, match);
1755
- groupFound = true;
1756
- break;
1757
- }
1758
- }
1759
- if (!groupFound) {
1760
- throw new Error('No group found for empty match');
1761
- }
1954
+ else {
1955
+ relations.push(createNothingToOneRelation({
1956
+ currentRound: preFinalLowerRound,
1957
+ nextRound: finalLowerRound,
1958
+ }));
1762
1959
  }
1763
- roundNumber++;
1764
1960
  }
1765
- return roundsWithSwissGroups;
1961
+ else {
1962
+ if (!previousLowerRound)
1963
+ throw new Error('previousLowerRound is null');
1964
+ relations.push(createOneToOneRelation({
1965
+ currentRound: finalLowerRound,
1966
+ previousRound: previousLowerRound,
1967
+ nextRound: currentUpperRound,
1968
+ rootRound: firstLowerRound,
1969
+ }));
1970
+ }
1766
1971
  };
1767
- const generateBracketDataForEthlete = (source) => {
1768
- const tournamentMode = generateTournamentModeFormEthleteRounds(source);
1769
- const bracketData = {
1770
- rounds: [],
1771
- matches: [],
1772
- mode: tournamentMode,
1773
- };
1774
- for (const currentItem of source) {
1775
- if (bracketData.rounds.some((r) => r.id === currentItem.round.id)) {
1776
- throw new Error(`Round with id ${currentItem.round.id} already exists in the bracket data.`);
1777
- }
1778
- const roundType = generateRoundTypeFromEthleteRoundType(currentItem.round.type, tournamentMode, currentItem.matches.length);
1779
- const bracketRound = {
1780
- type: roundType,
1781
- id: currentItem.round.id,
1782
- data: currentItem.round,
1783
- name: currentItem.round.name || currentItem.round.type,
1784
- };
1785
- bracketData.rounds.push(bracketRound);
1786
- for (const match of currentItem.matches) {
1787
- if (bracketData.matches.some((m) => m.id === match.id)) {
1788
- throw new Error(`Match with id ${match.id} already exists in the bracket data.`);
1789
- }
1790
- const bracketMatch = {
1791
- id: match.id,
1792
- data: match,
1793
- roundId: currentItem.round.id,
1794
- home: match.home?.id || null,
1795
- away: match.away?.id || null,
1796
- winner: match.winningSide,
1797
- status: match.status === 'published' ? 'completed' : 'pending',
1798
- };
1799
- bracketData.matches.push(bracketMatch);
1800
- }
1972
+ const handleFirstRound = (params) => {
1973
+ const { relations, currentUpperRound, nextUpperRound, lowerRounds, currentUpperRoundIndex } = params;
1974
+ relations.push(createNothingToOneRelation({
1975
+ currentRound: currentUpperRound,
1976
+ nextRound: nextUpperRound,
1977
+ }));
1978
+ const currentLowerRound = lowerRounds[currentUpperRoundIndex] || null;
1979
+ const nextLowerRound = lowerRounds[currentUpperRoundIndex + 1] || null;
1980
+ if (currentLowerRound && nextLowerRound) {
1981
+ relations.push(createNothingToOneRelation({
1982
+ currentRound: currentLowerRound,
1983
+ nextRound: nextLowerRound,
1984
+ }));
1801
1985
  }
1802
- return bracketData;
1803
1986
  };
1804
- const generateAndSetBracketMatchWithParticipants = (match, options) => {
1805
- const matchId = match.id;
1806
- const { bracketData, bracketRound, currentIndexInRound, participantCounter } = options;
1807
- let localParticipantCounter = participantCounter;
1808
- const bracketMatch = {
1809
- id: matchId,
1810
- indexInRound: currentIndexInRound,
1811
- position: (currentIndexInRound + 1),
1812
- data: match.data,
1813
- round: bracketRound,
1814
- home: null,
1815
- away: null,
1816
- status: match.status,
1817
- winnerSide: match.winner,
1818
- winner: null,
1819
- };
1820
- bracketData.matches.set(matchId, bracketMatch);
1821
- bracketRound.matches.set(matchId, bracketMatch);
1822
- const participantIds = [match.home, match.away];
1823
- for (const [participantIndex, participantId] of participantIds.entries()) {
1824
- if (!participantId)
1825
- continue;
1826
- const side = participantIndex === 0 ? 'home' : 'away';
1827
- if (!bracketData.participants.has(participantId)) {
1828
- bracketData.participants.set(participantId, {
1829
- id: participantId,
1830
- shortId: `p${localParticipantCounter++}`,
1831
- name: `${side} ${currentIndexInRound}`,
1832
- matches: new Map(),
1987
+ const handleRegularRound = (params) => {
1988
+ const { relations, currentUpperRound, previousUpperRound, nextUpperRound, lowerRounds, currentUpperRoundIndex, firstUpperRound, firstLowerRound, } = params;
1989
+ relations.push(createOneToOneRelation({
1990
+ currentRound: currentUpperRound,
1991
+ previousRound: previousUpperRound,
1992
+ nextRound: nextUpperRound,
1993
+ rootRound: firstUpperRound,
1994
+ }));
1995
+ const currentLowerRound = lowerRounds[currentUpperRoundIndex] || null;
1996
+ const previousLowerRound = lowerRounds[currentUpperRoundIndex - 1] || null;
1997
+ const nextLowerRound = lowerRounds[currentUpperRoundIndex + 1] || null;
1998
+ if (currentLowerRound &&
1999
+ currentUpperRound.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET &&
2000
+ previousLowerRound &&
2001
+ nextLowerRound &&
2002
+ firstLowerRound) {
2003
+ relations.push(createOneToOneRelation({
2004
+ currentRound: currentLowerRound,
2005
+ previousRound: previousLowerRound,
2006
+ nextRound: nextLowerRound,
2007
+ rootRound: firstLowerRound,
2008
+ }));
2009
+ }
2010
+ };
2011
+ const generateRoundRelationsNew = (bracketData) => {
2012
+ const relations = [];
2013
+ const allRounds = [...bracketData.rounds.values()];
2014
+ const upperRounds = allRounds.filter((r) => r.type !== DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET);
2015
+ const lowerRounds = allRounds.filter((r) => r.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET);
2016
+ const firstUpperRound = upperRounds[0];
2017
+ const firstLowerRound = lowerRounds[0] || null;
2018
+ const lastLowerRound = lowerRounds[lowerRounds.length - 1] || null;
2019
+ if (!firstUpperRound)
2020
+ throw new Error('No upper rounds found');
2021
+ const hasLowerRounds = lowerRounds.length > 0;
2022
+ for (const [currentUpperRoundIndex] of upperRounds.entries()) {
2023
+ const nav = getNavigationContext({
2024
+ upperRounds,
2025
+ currentUpperRoundIndex,
2026
+ });
2027
+ if (nav.isFinal && hasLowerRounds && lastLowerRound && firstLowerRound && nav.previousUpperRound) {
2028
+ handleFinalRound({
2029
+ relations,
2030
+ currentUpperRound: nav.currentUpperRound,
2031
+ previousUpperRound: nav.previousUpperRound,
2032
+ nextUpperRound: nav.nextUpperRound,
2033
+ lowerRounds,
2034
+ currentUpperRoundIndex,
2035
+ firstUpperRound,
2036
+ firstLowerRound,
2037
+ lastLowerRound,
1833
2038
  });
1834
2039
  }
1835
- const participantData = bracketData.participants.get(participantId);
1836
- if (!participantData.matches.has(bracketMatch.id)) {
1837
- participantData.matches.set(bracketMatch.id, {
1838
- side: match.home === participantId ? 'home' : 'away',
1839
- bracketMatch,
2040
+ else if (nav.isLastUpperRound && nav.previousUpperRound) {
2041
+ relations.push(createOneToNothingRelation({
2042
+ currentRound: nav.currentUpperRound,
2043
+ previousRound: nav.previousUpperRound,
2044
+ rootRound: firstUpperRound,
2045
+ }));
2046
+ }
2047
+ else if (nav.currentUpperRound.isFirstRound && nav.nextUpperRound) {
2048
+ handleFirstRound({
2049
+ relations,
2050
+ currentUpperRound: nav.currentUpperRound,
2051
+ nextUpperRound: nav.nextUpperRound,
2052
+ lowerRounds,
2053
+ currentUpperRoundIndex,
1840
2054
  });
1841
2055
  }
1842
- bracketMatch[side] = participantData;
1843
- if (bracketMatch.winnerSide === side) {
1844
- bracketMatch.winner = participantData;
2056
+ else if (nav.previousUpperRound && nav.nextUpperRound) {
2057
+ handleRegularRound({
2058
+ relations,
2059
+ currentUpperRound: nav.currentUpperRound,
2060
+ previousUpperRound: nav.previousUpperRound,
2061
+ nextUpperRound: nav.nextUpperRound,
2062
+ lowerRounds,
2063
+ currentUpperRoundIndex,
2064
+ firstUpperRound,
2065
+ firstLowerRound,
2066
+ });
1845
2067
  }
1846
2068
  }
1847
- return bracketMatch;
2069
+ return relations;
1848
2070
  };
1849
- const generateBracketData = (source, options) => {
1850
- if (!canRenderLayoutInTournamentMode(options.layout, source.mode)) {
1851
- throw new Error(`Cannot render layout ${options.layout} in mode ${source.mode}`);
1852
- }
1853
- const shouldSplitRoundsInTwo = options.layout === BRACKET_DATA_LAYOUT.MIRRORED;
1854
- const bracketData = {
1855
- matches: new Map(),
1856
- mode: source.mode,
1857
- participants: new Map(),
1858
- rounds: new Map(),
1859
- roundIds: [],
1860
- matchIds: [],
1861
- };
1862
- let currentUpperBracketIndex = 0;
1863
- let currentLowerBracketIndex = 0;
1864
- const splitRoundsRest = [];
1865
- for (const [roundIndex, round] of source.rounds.entries()) {
1866
- const isLowerBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
1867
- const matches = source.matches.filter((m) => m.roundId === round.id);
1868
- const roundId = round.id;
1869
- const shouldSplitRound = shouldSplitRoundsInTwo && matches.length % 2 === 0;
1870
- const isFirstRound = roundIndex === 0;
1871
- if (shouldSplitRound) {
1872
- const firstHalfMatchesMaxIndex = matches.length / 2 - 1;
1873
- const firstHalfRoundId = `${roundId}--half-1`;
1874
- const secondHalfRoundId = `${roundId}--half-2`;
1875
- const bracketRoundFirstHalf = {
1876
- type: round.type,
1877
- id: firstHalfRoundId,
1878
- index: isLowerBracket ? currentLowerBracketIndex : currentUpperBracketIndex,
1879
- data: round.data,
1880
- position: ((isLowerBracket ? currentLowerBracketIndex : currentUpperBracketIndex) + 1),
1881
- name: round.name,
1882
- matchCount: matches.length / 2,
1883
- matches: new Map(),
1884
- mirrorRoundType: BRACKET_ROUND_MIRROR_TYPE.LEFT,
1885
- isFirstRound,
1886
- };
1887
- const bracketRoundSecondHalf = {
1888
- type: round.type,
1889
- id: secondHalfRoundId,
1890
- index: -1,
1891
- data: round.data,
1892
- position: -1,
1893
- name: round.name,
1894
- matchCount: matches.length / 2,
1895
- matches: new Map(),
1896
- mirrorRoundType: BRACKET_ROUND_MIRROR_TYPE.RIGHT,
1897
- isFirstRound,
1898
- };
1899
- bracketData.roundIds.push(firstHalfRoundId);
1900
- bracketData.rounds.set(firstHalfRoundId, bracketRoundFirstHalf);
1901
- splitRoundsRest.unshift(bracketRoundSecondHalf);
1902
- for (let index = 0; index <= firstHalfMatchesMaxIndex; index++) {
1903
- const matchFirst = matches[index];
1904
- const matchSecond = matches[firstHalfMatchesMaxIndex + 1 + index];
1905
- if (!matchFirst || !matchSecond)
1906
- throw new Error('Match not found');
1907
- const participantCounter = bracketData.participants.size;
1908
- const bracketMatchFirst = generateAndSetBracketMatchWithParticipants(matchFirst, {
1909
- bracketData,
1910
- bracketRound: bracketRoundFirstHalf,
1911
- currentIndexInRound: index,
1912
- participantCounter,
1913
- });
1914
- // The participant counter might have changed after the first half
1915
- const updatedParticipantCounter = bracketData.participants.size;
1916
- const bracketMatchSecond = generateAndSetBracketMatchWithParticipants(matchSecond, {
1917
- bracketData,
1918
- bracketRound: bracketRoundSecondHalf,
1919
- currentIndexInRound: index,
1920
- participantCounter: updatedParticipantCounter,
1921
- });
1922
- bracketData.matchIds.push(bracketMatchFirst.id, bracketMatchSecond.id);
1923
- }
1924
- }
1925
- else {
1926
- const bracketRound = {
1927
- type: round.type,
1928
- id: roundId,
1929
- index: isLowerBracket ? currentLowerBracketIndex : currentUpperBracketIndex,
1930
- data: round.data,
1931
- position: ((isLowerBracket ? currentLowerBracketIndex : currentUpperBracketIndex) + 1),
1932
- name: round.name,
1933
- matchCount: matches.length,
1934
- matches: new Map(),
1935
- mirrorRoundType: null,
1936
- isFirstRound,
1937
- };
1938
- bracketData.roundIds.push(roundId);
1939
- bracketData.rounds.set(roundId, bracketRound);
1940
- let currentIndexInRound = 0;
1941
- for (const match of matches) {
1942
- const participantCounter = bracketData.participants.size;
1943
- const bracketMatch = generateAndSetBracketMatchWithParticipants(match, {
1944
- bracketData,
1945
- bracketRound,
1946
- currentIndexInRound,
1947
- participantCounter,
1948
- });
1949
- bracketData.matchIds.push(bracketMatch.id);
1950
- currentIndexInRound++;
1951
- }
2071
+
2072
+ const createNewBracket = (source, options) => {
2073
+ const bracketNewBase = createNewBracketBase(source, options);
2074
+ const rounds = new BracketMap();
2075
+ const roundsByType = new BracketMap();
2076
+ for (const roundBase of bracketNewBase.rounds.values()) {
2077
+ const newRound = {
2078
+ ...roundBase,
2079
+ matches: new BracketMap(),
2080
+ relation: { type: 'dummy' },
2081
+ };
2082
+ rounds.set(roundBase.id, newRound);
2083
+ if (!roundsByType.has(roundBase.type)) {
2084
+ roundsByType.set(roundBase.type, new BracketMap());
1952
2085
  }
1953
- if (isLowerBracket) {
1954
- currentLowerBracketIndex++;
2086
+ roundsByType.getOrThrow(roundBase.type).set(roundBase.id, newRound);
2087
+ }
2088
+ const participants = new BracketMap();
2089
+ for (const participantBase of bracketNewBase.participants.values()) {
2090
+ participants.set(participantBase.id, {
2091
+ ...participantBase,
2092
+ matches: new BracketMap(),
2093
+ });
2094
+ }
2095
+ const matches = new BracketMap();
2096
+ for (const matchBase of bracketNewBase.matches.values()) {
2097
+ const round = rounds.getOrThrow(matchBase.roundId);
2098
+ const homeParticipant = matchBase.home
2099
+ ? { ...matchBase.home, matches: new BracketMap() }
2100
+ : null;
2101
+ const awayParticipant = matchBase.away
2102
+ ? { ...matchBase.away, matches: new BracketMap() }
2103
+ : null;
2104
+ const newMatch = {
2105
+ ...matchBase,
2106
+ home: homeParticipant,
2107
+ away: awayParticipant,
2108
+ winner: null,
2109
+ round,
2110
+ relation: { type: 'dummy' },
2111
+ };
2112
+ if (matchBase.winner) {
2113
+ const winnerParticipant = homeParticipant?.id === matchBase.winner.id ? homeParticipant : awayParticipant;
2114
+ if (!winnerParticipant)
2115
+ throw new Error(`Winner participant with id ${matchBase.winner.id} not found in match base`);
2116
+ newMatch.winner = winnerParticipant;
2117
+ }
2118
+ matches.set(matchBase.id, newMatch);
2119
+ round.matches.set(matchBase.id, newMatch);
2120
+ if (homeParticipant) {
2121
+ const participant = participants.getOrThrow(homeParticipant.id);
2122
+ participant.matches.set(matchBase.id, {
2123
+ ...newMatch,
2124
+ me: homeParticipant,
2125
+ opponent: awayParticipant,
2126
+ });
1955
2127
  }
1956
- else {
1957
- currentUpperBracketIndex++;
2128
+ if (awayParticipant) {
2129
+ const participant = participants.getOrThrow(awayParticipant.id);
2130
+ participant.matches.set(matchBase.id, {
2131
+ ...newMatch,
2132
+ me: awayParticipant,
2133
+ opponent: homeParticipant,
2134
+ });
1958
2135
  }
1959
2136
  }
1960
- if (splitRoundsRest.length) {
1961
- const lastRoundId = bracketData.roundIds[bracketData.roundIds.length - 1];
1962
- if (!lastRoundId)
1963
- throw new Error('Last round id not found');
1964
- const lastRound = bracketData.rounds.get(lastRoundId);
1965
- if (!lastRound)
1966
- throw new Error('Last round not found');
1967
- for (const [splitRoundIndex, splitRound] of splitRoundsRest.entries()) {
1968
- splitRound.index = lastRound.index + splitRoundIndex + 1;
1969
- splitRound.position = (lastRound.position + splitRoundIndex + 1);
1970
- bracketData.rounds.set(splitRound.id, splitRound);
1971
- bracketData.roundIds.push(splitRound.id);
2137
+ for (const participant of participants.values()) {
2138
+ for (const match of participant.matches.values()) {
2139
+ if (match.home?.id === participant.id)
2140
+ match.home.matches = participant.matches;
2141
+ if (match.away?.id === participant.id)
2142
+ match.away.matches = participant.matches;
2143
+ if (match.winner?.id === participant.id)
2144
+ match.winner.matches = participant.matches;
1972
2145
  }
1973
2146
  }
1974
- return bracketData;
1975
- };
1976
- const generateTournamentModeFormGgData = (source) => {
1977
- switch (source.mode) {
1978
- // case 'groups':
1979
- // return TOURNAMENT_MODE.GROUP;
1980
- // case 'fifa_swiss': {
1981
- // const lastRound = source[source.length - 1];
1982
- // if (!lastRound) throw new Error('No last round found');
1983
- // if (lastRound.matches.length !== firstRound.matches.length) {
1984
- // return TOURNAMENT_MODE.SWISS_WITH_ELIMINATION;
1985
- // } else {
1986
- // return TOURNAMENT_MODE.SWISS;
1987
- // }
1988
- // }
1989
- case 'double-elimination':
1990
- return TOURNAMENT_MODE.DOUBLE_ELIMINATION;
1991
- case 'single-elimination':
1992
- return TOURNAMENT_MODE.SINGLE_ELIMINATION;
1993
- default:
1994
- throw new Error(`Unsupported tournament mode: ${source.mode}`);
2147
+ const newBracket = {
2148
+ matches,
2149
+ participants,
2150
+ rounds,
2151
+ roundsByType,
2152
+ mode: bracketNewBase.mode,
2153
+ };
2154
+ const roundRelations = generateRoundRelationsNew(newBracket);
2155
+ for (const roundRelation of roundRelations) {
2156
+ roundRelation.currentRound.relation = roundRelation;
2157
+ }
2158
+ const matchRelations = generateMatchRelationsNew(newBracket);
2159
+ for (const matchRelation of matchRelations) {
2160
+ matchRelation.currentMatch.relation = matchRelation;
1995
2161
  }
2162
+ return newBracket;
1996
2163
  };
1997
- const generateRoundTypeFromGgMatch = (tournamentMode, bracketType, // 'winner' | 'looser' | null,
1998
- stageNumber, matchCount) => {
1999
- switch (tournamentMode) {
2000
- case 'double-elimination': {
2001
- switch (stageNumber) {
2002
- case 1: {
2003
- switch (bracketType) {
2004
- case 'winner':
2005
- return DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET;
2006
- case 'looser':
2007
- return DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
2008
- default:
2009
- throw new Error(`Unsupported bracket type: ${bracketType}`);
2010
- }
2011
- }
2012
- case 2: {
2013
- return COMMON_BRACKET_ROUND_TYPE.FINAL;
2014
- }
2015
- case 3: {
2016
- return DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL;
2017
- }
2018
- default: {
2019
- throw new Error(`Unsupported stage number: ${stageNumber}`);
2020
- }
2021
- }
2022
- }
2023
- case 'single-elimination': {
2024
- if (matchCount === 1) {
2025
- return COMMON_BRACKET_ROUND_TYPE.FINAL;
2026
- }
2027
- else {
2028
- return SINGLE_ELIMINATION_BRACKET_ROUND_TYPE.SINGLE_ELIMINATION_BRACKET;
2029
- }
2164
+
2165
+ const pad = (str, len) => str.padEnd(len, ' ');
2166
+ const color = (str, code) => `\x1b[${code}m${str}\x1b[0m`;
2167
+ const roundColor = (str) => color(str, 36); // cyan
2168
+ const arrowColor = (str) => color(str, 90); // gray
2169
+ const labelColor = (str) => color(str, 33); // yellow
2170
+ const factorColor = (str) => color(str, 32); // green
2171
+ const logRoundRelations = (bracketData) => {
2172
+ // Find max round name length for alignment
2173
+ let maxNameLen = 0;
2174
+ for (const round of bracketData.rounds.values()) {
2175
+ maxNameLen = Math.max(maxNameLen, round.name.length);
2176
+ }
2177
+ const colWidth = maxNameLen + 2;
2178
+ // Build rows
2179
+ const rows = [];
2180
+ for (const round of bracketData.rounds.values()) {
2181
+ const relation = round.relation;
2182
+ switch (relation.type) {
2183
+ case 'nothing-to-one':
2184
+ rows.push([
2185
+ labelColor(pad('START', colWidth)),
2186
+ arrowColor('──▶'),
2187
+ roundColor(pad(round.name, colWidth)),
2188
+ arrowColor('──▶'),
2189
+ roundColor(pad(relation.nextRound.name, colWidth)),
2190
+ factorColor(`[${relation.nextRoundMatchFactor}]`),
2191
+ ]);
2192
+ break;
2193
+ case 'one-to-nothing':
2194
+ rows.push([
2195
+ roundColor(pad(relation.previousRound.name, colWidth)),
2196
+ arrowColor('──▶'),
2197
+ roundColor(pad(round.name, colWidth)),
2198
+ arrowColor('──▶'),
2199
+ labelColor(pad('END', colWidth)),
2200
+ factorColor(`[${relation.previousRoundMatchFactor}]`),
2201
+ ]);
2202
+ break;
2203
+ case 'one-to-one':
2204
+ rows.push([
2205
+ roundColor(pad(relation.previousRound.name, colWidth)),
2206
+ arrowColor('──▶'),
2207
+ roundColor(pad(round.name, colWidth)),
2208
+ arrowColor('──▶'),
2209
+ roundColor(pad(relation.nextRound.name, colWidth)),
2210
+ factorColor(`[Prev: ${relation.previousRoundMatchFactor}, Next: ${relation.nextRoundMatchFactor}]`),
2211
+ ]);
2212
+ break;
2213
+ case 'two-to-one':
2214
+ rows.push([
2215
+ roundColor(pad(relation.previousUpperRound.name, colWidth)),
2216
+ arrowColor(' │ '),
2217
+ roundColor(pad(relation.previousLowerRound.name, colWidth)),
2218
+ arrowColor('──▶'),
2219
+ roundColor(pad(round.name, colWidth)),
2220
+ arrowColor('──▶'),
2221
+ roundColor(pad(relation.nextRound.name, colWidth)),
2222
+ factorColor(`[Upper: ${relation.previousUpperRoundMatchFactor}, Lower: ${relation.previousLowerRoundMatchFactor}, Next: ${relation.nextRoundMatchFactor}]`),
2223
+ ]);
2224
+ break;
2225
+ case 'two-to-nothing':
2226
+ rows.push([
2227
+ roundColor(pad(relation.previousUpperRound.name, colWidth)),
2228
+ arrowColor(' │'),
2229
+ roundColor(pad(relation.previousLowerRound.name, colWidth)),
2230
+ arrowColor(' ▼'),
2231
+ roundColor(pad(round.name, colWidth)),
2232
+ arrowColor('──▶'),
2233
+ labelColor(pad('END', colWidth)),
2234
+ factorColor(`[Upper: ${relation.previousUpperRoundMatchFactor}, Lower: ${relation.previousLowerRoundMatchFactor}]`),
2235
+ ]);
2236
+ break;
2030
2237
  }
2031
- default: {
2032
- throw new Error(`Unsupported tournament mode: ${tournamentMode}`);
2238
+ }
2239
+ // Print header
2240
+ const divider = (label) => {
2241
+ // eslint-disable-next-line no-control-regex
2242
+ const width = rows[0]?.reduce((w, col) => w + col.replace(/\x1b\[[0-9;]*m/g, '').length + 2, 0) || 60;
2243
+ return `\n${'='.repeat(width)}\n${labelColor(label)}\n${'='.repeat(width)}\n`;
2244
+ };
2245
+ console.log(divider('Bracket Structure'));
2246
+ // Print rows
2247
+ for (const row of rows) {
2248
+ console.log(row.join(' '));
2249
+ }
2250
+ console.log();
2251
+ };
2252
+
2253
+ const FIRST_ROUNDS_TYPE = {
2254
+ SINGLE: 'single',
2255
+ DOUBLE: 'double',
2256
+ };
2257
+ const getFirstRounds = (bracketData) => {
2258
+ if (bracketData.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION) {
2259
+ const upper = bracketData.roundsByType.getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET).first();
2260
+ const lower = bracketData.roundsByType.getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET).first();
2261
+ if (!upper || !lower) {
2262
+ throw new Error('Upper or lower first round is null');
2033
2263
  }
2264
+ return {
2265
+ type: FIRST_ROUNDS_TYPE.DOUBLE,
2266
+ upper,
2267
+ lower,
2268
+ };
2034
2269
  }
2270
+ const first = bracketData.rounds.first();
2271
+ if (!first)
2272
+ throw new Error('First round is null');
2273
+ return {
2274
+ type: FIRST_ROUNDS_TYPE.SINGLE,
2275
+ first,
2276
+ };
2035
2277
  };
2036
- const generateBracketDataForGg = (source) => {
2037
- const tournamentMode = generateTournamentModeFormGgData(source);
2038
- const bracketData = {
2039
- rounds: [],
2040
- matches: [],
2041
- mode: tournamentMode,
2278
+
2279
+ const factorialCache = new Map();
2280
+ const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
2281
+ const ADVANCE_WINS = 3;
2282
+ const ELIMINATE_LOSSES = 3;
2283
+ // Cache factorial calculations
2284
+ const getFactorial = (n) => {
2285
+ if (n <= 1)
2286
+ return 1;
2287
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2288
+ if (factorialCache.has(n))
2289
+ return factorialCache.get(n);
2290
+ const result = n * getFactorial(n - 1);
2291
+ factorialCache.set(n, result);
2292
+ return result;
2042
2293
  };
2043
- const matchesGrouped = new Map();
2044
- for (const match of source.matches) {
2045
- const roundId = `${match.roundNumber}-${match.stageNumber}-${match.bracketType || null}`;
2046
- if (!matchesGrouped.has(roundId)) {
2047
- matchesGrouped.set(roundId, []);
2048
- }
2049
- matchesGrouped.get(roundId)?.push(match);
2294
+ // Pre-calculate roundFactorial
2295
+ const roundFactorial = getFactorial(roundNumber);
2296
+ let totalCombinations = 0;
2297
+ const validGroups = [];
2298
+ // Single loop to gather valid groups and total combinations
2299
+ for (let wins = roundNumber; wins >= 0; wins--) {
2300
+ const losses = roundNumber - wins;
2301
+ const remainingGames = ADVANCE_WINS + ELIMINATE_LOSSES - (wins + losses) - 1;
2302
+ const notYetEliminated = losses < ELIMINATE_LOSSES;
2303
+ const canStillAdvance = wins < ADVANCE_WINS && remainingGames >= 0;
2304
+ if (!canStillAdvance || !notYetEliminated)
2305
+ continue;
2306
+ const combinations = roundFactorial / (getFactorial(wins) * getFactorial(losses));
2307
+ totalCombinations += combinations;
2308
+ validGroups.push({ wins, losses, combinations });
2050
2309
  }
2051
- for (const [roundId, matches] of matchesGrouped.entries()) {
2052
- const firstMatch = matches[0];
2053
- if (!firstMatch)
2054
- throw new Error('First match not found');
2055
- const roundType = generateRoundTypeFromGgMatch(tournamentMode, firstMatch.bracketType, firstMatch.stageNumber, matches.length);
2056
- const bracketRound = {
2057
- type: roundType,
2058
- id: roundId,
2059
- data: null,
2060
- name: firstMatch.roundTitle,
2310
+ // Create final groups with calculated proportions
2311
+ return validGroups.map(({ wins, losses, combinations }) => ({
2312
+ id: `${wins}-${losses}`,
2313
+ name: `${wins}-${losses}`,
2314
+ matchesInGroup: Math.round((combinations / totalCombinations) * totalMatchesInRound),
2315
+ }));
2316
+ };
2317
+ const generateBracketRoundSwissGroupMaps = (bracketData) => {
2318
+ if (bracketData.mode !== TOURNAMENT_MODE.SWISS_WITH_ELIMINATION) {
2319
+ return null;
2320
+ }
2321
+ const roundsWithSwissGroups = new Map();
2322
+ let roundNumber = 0;
2323
+ for (const bracketRound of bracketData.rounds.values()) {
2324
+ const availableGroups = getAvailableSwissGroupsForRound(roundNumber, bracketRound.matchCount);
2325
+ const roundSwissData = {
2326
+ groups: new Map(),
2061
2327
  };
2062
- bracketData.rounds.push(bracketRound);
2063
- for (const match of matches) {
2064
- const bracketMatch = {
2065
- id: match.id,
2066
- data: match,
2067
- roundId: roundId,
2068
- home: match.homeMatchSide.participant?.id || null,
2069
- away: match.awayMatchSide.participant?.id || null,
2070
- winner: match.winningSide,
2071
- status: match.status === 'completed' ? 'completed' : 'pending',
2328
+ for (const group of availableGroups) {
2329
+ const subGroup = {
2330
+ id: group.id,
2331
+ name: group.name,
2332
+ matches: new BracketMap(),
2333
+ allowedMatchCount: group.matchesInGroup,
2072
2334
  };
2073
- bracketData.matches.push(bracketMatch);
2335
+ roundSwissData.groups.set(group.id, subGroup);
2336
+ }
2337
+ const emptyMatchIds = [];
2338
+ for (const match of bracketRound.matches.values()) {
2339
+ const anyParticipant = match.home || match.away;
2340
+ if (!anyParticipant) {
2341
+ emptyMatchIds.push(match.id);
2342
+ continue;
2343
+ }
2344
+ const wins = anyParticipant.winCount;
2345
+ const losses = anyParticipant.lossCount;
2346
+ const group = roundSwissData.groups.get(`${wins}-${losses}`);
2347
+ if (!group)
2348
+ throw new Error('Group not found for match: ' + match.id);
2349
+ group.matches.set(match.id, match);
2350
+ }
2351
+ for (const emptyMatchId of emptyMatchIds) {
2352
+ const match = bracketRound.matches.getOrThrow(emptyMatchId);
2353
+ let groupFound = false;
2354
+ for (const group of roundSwissData.groups.values()) {
2355
+ if (group.matches.size < group.allowedMatchCount) {
2356
+ group.matches.set(match.id, match);
2357
+ groupFound = true;
2358
+ break;
2359
+ }
2360
+ }
2361
+ if (!groupFound) {
2362
+ throw new Error('No group found for empty match');
2363
+ }
2074
2364
  }
2365
+ roundNumber++;
2075
2366
  }
2076
- return bracketData;
2367
+ return roundsWithSwissGroups;
2077
2368
  };
2078
2369
 
2079
2370
  const path = (d, options) => `<path d="${d.replace(/\s+/g, ' ').trim()}" stroke="currentColor" fill="none" stroke-width="${options.width}" stroke-dasharray="${options.dashArray}" stroke-dashoffset="${options.dashOffset}" class="${options.className}" />`;
2080
- const curve = (from, to, direction, options) => {
2081
- const fromInline = from.inline.end;
2082
- const toInline = to.inline.start;
2083
- const toBlock = to.block.center;
2084
- const fromBlock = from.block.center;
2085
- const startingCurveAmount = options.lineStartingCurveAmount;
2086
- const endingCurveAmount = options.lineEndingCurveAmount;
2087
- const totalSpace = toInline - fromInline;
2088
- const totalSpaceMinusCurve = totalSpace - startingCurveAmount - endingCurveAmount;
2089
- const lineLength = totalSpaceMinusCurve / 2;
2090
- const straightLeftEnd = fromInline + lineLength;
2091
- const straightRightStart = toInline - lineLength;
2092
- // first 90 degree curve down
2093
- const firstCurveStartX = straightLeftEnd;
2094
- const firstCurveEndX = straightLeftEnd + startingCurveAmount;
2095
- const firstCurveEndY = direction === 'down' ? fromBlock + startingCurveAmount : fromBlock - startingCurveAmount;
2096
- const firstCurveBezierX = straightLeftEnd + startingCurveAmount;
2097
- const firstCurveBezierY = fromBlock;
2098
- // second 90 degree curve to the right
2099
- const secondCurveStartY = direction === 'down' ? toBlock - endingCurveAmount : toBlock + endingCurveAmount;
2100
- const secondCurveEndX = straightRightStart;
2101
- const secondCurveEndY = toBlock;
2102
- const secondCurveBezierX = straightRightStart - endingCurveAmount;
2103
- const secondCurveBezierY = toBlock;
2104
- const pathStr = `
2105
- M ${fromInline} ${fromBlock}
2106
- H ${firstCurveStartX}
2107
- Q ${firstCurveBezierX} ${firstCurveBezierY}, ${firstCurveEndX} ${firstCurveEndY}
2108
- V ${secondCurveStartY}
2109
- Q ${secondCurveBezierX} ${secondCurveBezierY}, ${secondCurveEndX} ${secondCurveEndY}
2110
- H ${toInline}
2111
- `;
2112
- return path(pathStr, options.path);
2113
- };
2114
- const curveInverted = (from, to, direction, options) => {
2115
- const fromInline = from.inline.start;
2371
+ const curvePath = (from, to, direction, options) => {
2372
+ const inverted = options.inverted ?? false;
2373
+ // Inline/block coordinates depending on direction and inversion
2374
+ const fromInline = inverted ? from.inline.start : from.inline.end;
2375
+ const toInline = inverted ? to.inline.end : to.inline.start;
2116
2376
  const fromBlock = from.block.center;
2117
- const toInline = to.inline.end;
2118
2377
  const toBlock = to.block.center;
2119
- const startingCurveAmount = options.lineStartingCurveAmount;
2120
- const endingCurveAmount = options.lineEndingCurveAmount;
2121
- const totalSpace = fromInline - toInline;
2122
- const totalSpaceMinusCurve = totalSpace - startingCurveAmount - endingCurveAmount;
2123
- const lineLength = totalSpaceMinusCurve / 2;
2124
- const straightRightEnd = fromInline - lineLength;
2125
- const straightLeftStart = toInline + lineLength;
2126
- const firstCurveStartX = straightRightEnd;
2127
- const firstCurveEndX = straightRightEnd - startingCurveAmount;
2128
- const firstCurveEndY = direction === 'down' ? fromBlock + startingCurveAmount : fromBlock - startingCurveAmount;
2129
- const firstCurveBezierX = straightRightEnd - startingCurveAmount;
2130
- const firstCurveBezierY = fromBlock;
2131
- // second 90 degree curve to the right
2132
- const secondCurveStartY = direction === 'down' ? toBlock - endingCurveAmount : toBlock + endingCurveAmount;
2133
- const secondCurveEndX = straightLeftStart;
2378
+ // Curve parameters
2379
+ const startCurve = options.lineStartingCurveAmount;
2380
+ const endCurve = options.lineEndingCurveAmount;
2381
+ const totalInline = Math.abs(toInline - fromInline);
2382
+ const straightLength = (totalInline - startCurve - endCurve) / 2;
2383
+ // Calculate key points for the path
2384
+ const straightEnd = inverted ? fromInline - straightLength : fromInline + straightLength;
2385
+ const straightStart = inverted ? toInline + straightLength : toInline - straightLength;
2386
+ // First curve (from start)
2387
+ const firstCurveEndX = inverted ? straightEnd - startCurve : straightEnd + startCurve;
2388
+ const firstCurveEndY = direction === 'down' ? fromBlock + startCurve : fromBlock - startCurve;
2389
+ // Second curve (to end)
2390
+ const secondCurveStartY = direction === 'down' ? toBlock - endCurve : toBlock + endCurve;
2391
+ const secondCurveEndX = straightStart;
2134
2392
  const secondCurveEndY = toBlock;
2135
- const secondCurveBezierX = straightLeftStart + endingCurveAmount;
2136
- const secondCurveBezierY = toBlock;
2137
- const pathStr = `
2138
- M ${fromInline} ${fromBlock}
2139
- H ${firstCurveStartX}
2140
- Q ${firstCurveBezierX} ${firstCurveBezierY}, ${firstCurveEndX} ${firstCurveEndY}
2141
- V ${secondCurveStartY}
2142
- Q ${secondCurveBezierX} ${secondCurveBezierY}, ${secondCurveEndX} ${secondCurveEndY}
2143
- H ${toInline}
2144
- `;
2145
- return path(pathStr, options.path);
2393
+ const secondCurveBezierX = inverted ? straightStart + endCurve : straightStart - endCurve;
2394
+ // SVG path string
2395
+ const d = [
2396
+ `M ${fromInline} ${fromBlock}`,
2397
+ `H ${straightEnd}`,
2398
+ `Q ${firstCurveEndX} ${fromBlock}, ${firstCurveEndX} ${firstCurveEndY}`,
2399
+ `V ${secondCurveStartY}`,
2400
+ `Q ${secondCurveBezierX} ${toBlock}, ${secondCurveEndX} ${secondCurveEndY}`,
2401
+ `H ${toInline}`,
2402
+ ].join(' ');
2403
+ return path(d, options.path);
2146
2404
  };
2147
- const line = (from, to, options) => {
2405
+ const linePath = (from, to, options) => {
2148
2406
  return path(`M ${from.inline.end} ${from.block.center} L ${to.inline.start} ${to.block.center}`, options.path);
2149
2407
  };
2150
2408
  const getMatchPosition = (roundItem, matchId, staticMeasurements) => {
@@ -2236,24 +2494,25 @@ const drawMan = (items, firstRounds, dimensions) => {
2236
2494
  case 'one-to-one': {
2237
2495
  const previousMatchPosition = getMatchPosition(items.get(item.matchRelation.previousRound.id), item.matchRelation.previousMatch.id, staticMeasurements);
2238
2496
  // draw a straight line
2239
- svgParts.push(line(previousMatchPosition, currentMatchPosition, { path: pathOptions }));
2497
+ svgParts.push(linePath(previousMatchPosition, currentMatchPosition, { path: pathOptions }));
2240
2498
  break;
2241
2499
  }
2242
2500
  case 'two-to-nothing':
2243
2501
  case 'two-to-one': {
2244
2502
  const previousUpper = getMatchPosition(items.get(item.matchRelation.previousUpperRound.id), item.matchRelation.previousUpperMatch.id, staticMeasurements);
2245
2503
  const previousLower = getMatchPosition(items.get(item.matchRelation.previousLowerRound.id), item.matchRelation.previousLowerMatch.id, staticMeasurements);
2504
+ const invertCurve = item.roundRelation.currentRound.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.RIGHT;
2246
2505
  const curveOptions = {
2247
2506
  ...dimensions.curve,
2507
+ inverted: invertCurve,
2248
2508
  path: { ...dimensions.path, className: '' },
2249
2509
  };
2250
- const curveFn = item.roundRelation.currentRound.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.RIGHT ? curveInverted : curve;
2251
2510
  // draw two lines that merge into one in the middle
2252
- svgParts.push(curveFn(previousUpper, currentMatchPosition, 'down', {
2511
+ svgParts.push(curvePath(previousUpper, currentMatchPosition, 'down', {
2253
2512
  ...curveOptions,
2254
2513
  path: { ...curveOptions.path, className: item.matchRelation.previousUpperMatch.winner?.shortId || '' },
2255
2514
  }));
2256
- svgParts.push(curveFn(previousLower, currentMatchPosition, 'up', {
2515
+ svgParts.push(curvePath(previousLower, currentMatchPosition, 'up', {
2257
2516
  ...curveOptions,
2258
2517
  path: { ...curveOptions.path, className: item.matchRelation.previousLowerMatch.winner?.shortId || '' },
2259
2518
  }));
@@ -2262,7 +2521,7 @@ const drawMan = (items, firstRounds, dimensions) => {
2262
2521
  item.matchRelation.nextRound.mirrorRoundType === null) {
2263
2522
  // draw a straight line for the special case of connecting the final match to the mirrored semi final match
2264
2523
  const next = getMatchPosition(items.get(item.matchRelation.nextRound.id), item.matchRelation.nextMatch.id, staticMeasurements);
2265
- svgParts.push(line(next, currentMatchPosition, { path: pathOptions }));
2524
+ svgParts.push(linePath(next, currentMatchPosition, { path: pathOptions }));
2266
2525
  }
2267
2526
  break;
2268
2527
  }
@@ -2272,14 +2531,18 @@ const drawMan = (items, firstRounds, dimensions) => {
2272
2531
  return svgParts.join('');
2273
2532
  };
2274
2533
 
2275
- const generateColumnCount = (bracketData, roundTypeMap) => {
2276
- const thirdPlaceRoundSize = roundTypeMap.get(COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE)?.size || 0;
2534
+ const generateColumnCount = (bracketData) => {
2535
+ const thirdPlaceRoundSize = bracketData.roundsByType.get(COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE)?.size || 0;
2277
2536
  switch (bracketData.mode) {
2278
2537
  case TOURNAMENT_MODE.DOUBLE_ELIMINATION: {
2279
- const upperBracketSize = roundTypeMap.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)?.size || 0;
2280
- const lowerBracketSize = roundTypeMap.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)?.size || 0;
2538
+ const upperBracketSize = bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)?.size || 0;
2539
+ const lowerBracketSize = bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)?.size || 0;
2281
2540
  // the total columns are the bigger number of winner bracket rounds vs looser bracket rounds + all other rounds excluding third place
2282
- return bracketData.rounds.size - thirdPlaceRoundSize + Math.max(upperBracketSize, lowerBracketSize);
2541
+ return (bracketData.rounds.size -
2542
+ upperBracketSize -
2543
+ lowerBracketSize -
2544
+ thirdPlaceRoundSize +
2545
+ Math.max(upperBracketSize, lowerBracketSize));
2283
2546
  }
2284
2547
  default: {
2285
2548
  // the total columns are the amount of rounds excluding third place
@@ -2287,13 +2550,15 @@ const generateColumnCount = (bracketData, roundTypeMap) => {
2287
2550
  }
2288
2551
  }
2289
2552
  };
2290
- const generateRowCount = (bracketData, roundTypeMap, options) => {
2553
+ const generateRowCount = (bracketData, options) => {
2291
2554
  switch (bracketData.mode) {
2292
2555
  case TOURNAMENT_MODE.DOUBLE_ELIMINATION: {
2293
- const upperBracketSize = roundTypeMap.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)?.values().next()
2294
- .value?.matchCount;
2295
- const lowerBracketSize = roundTypeMap.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)?.values().next()
2296
- .value?.matchCount;
2556
+ const upperBracketSize = bracketData.roundsByType
2557
+ .getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)
2558
+ .first()?.matchCount;
2559
+ const lowerBracketSize = bracketData.roundsByType
2560
+ .getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)
2561
+ ?.first()?.matchCount;
2297
2562
  const roundHeadersCount = options.includeRoundHeaders ? 2 : 0;
2298
2563
  if (upperBracketSize === undefined || lowerBracketSize === undefined) {
2299
2564
  throw new Error('Upper or lower bracket size is undefined');
@@ -2312,42 +2577,14 @@ const generateRowCount = (bracketData, roundTypeMap, options) => {
2312
2577
  }
2313
2578
  }
2314
2579
  };
2315
- const generateBracketGridDefinitions = (bracketData, roundTypeMap, options) => {
2316
- const columnCount = generateColumnCount(bracketData, roundTypeMap);
2317
- const rowCount = generateRowCount(bracketData, roundTypeMap, options);
2580
+ const generateBracketGridDefinitions = (bracketData, options) => {
2581
+ const columnCount = generateColumnCount(bracketData);
2582
+ const rowCount = generateRowCount(bracketData, options);
2318
2583
  return { columnCount, rowCount };
2319
2584
  };
2320
2585
 
2321
- class NewBracketDefaultMatchComponent {
2322
- constructor() {
2323
- this.bracketRound = input.required();
2324
- this.bracketMatch = input.required();
2325
- }
2326
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultMatchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2327
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", type: NewBracketDefaultMatchComponent, isStandalone: true, selector: "et-new-bracket-default-match", inputs: { bracketRound: { classPropertyName: "bracketRound", publicName: "bracketRound", isSignal: true, isRequired: true, transformFunction: null }, bracketMatch: { classPropertyName: "bracketMatch", publicName: "bracketMatch", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-new-bracket-default-match-host" }, ngImport: i0, template: ` {{ bracketMatch().id }} `, isInline: true, styles: [".et-new-bracket-default-match-host{display:block;padding:8px;border:1px solid yellow;inline-size:250px;block-size:75px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;font-size:12px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
2328
- }
2329
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultMatchComponent, decorators: [{
2330
- type: Component,
2331
- args: [{ selector: 'et-new-bracket-default-match', template: ` {{ bracketMatch().id }} `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
2332
- class: 'et-new-bracket-default-match-host',
2333
- }, styles: [".et-new-bracket-default-match-host{display:block;padding:8px;border:1px solid yellow;inline-size:250px;block-size:75px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;font-size:12px}\n"] }]
2334
- }] });
2335
-
2336
- class NewBracketDefaultRoundHeaderComponent {
2337
- constructor() {
2338
- this.bracketRound = input.required();
2339
- }
2340
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultRoundHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2341
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", type: NewBracketDefaultRoundHeaderComponent, isStandalone: true, selector: "et-new-bracket-default-round-header", inputs: { bracketRound: { classPropertyName: "bracketRound", publicName: "bracketRound", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "et-new-bracket-default-round-header-host" }, ngImport: i0, template: ` {{ bracketRound().name }} `, isInline: true, styles: [".et-new-bracket-default-round-header-host{display:block;padding:8px;border:1px solid green;inline-size:250px;block-size:50px;display:flex;justify-content:center;align-items:center;box-sizing:border-box}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
2342
- }
2343
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketDefaultRoundHeaderComponent, decorators: [{
2344
- type: Component,
2345
- args: [{ selector: 'et-new-bracket-default-round-header', template: ` {{ bracketRound().name }} `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
2346
- class: 'et-new-bracket-default-round-header-host',
2347
- }, styles: [".et-new-bracket-default-round-header-host{display:block;padding:8px;border:1px solid green;inline-size:250px;block-size:50px;display:flex;justify-content:center;align-items:center;box-sizing:border-box}\n"] }]
2348
- }] });
2349
-
2350
- const generateBracketGridItems = (bracketData, roundTypeMap, swissGroups, roundRelations, matchRelations, options) => {
2586
+ const generateBracketGridItems = (data) => {
2587
+ const { bracketData, swissGroups, options } = data;
2351
2588
  const roundHeaderComponent = options.headerComponent ?? NewBracketDefaultRoundHeaderComponent;
2352
2589
  const matchComponent = options.matchComponent ?? NewBracketDefaultMatchComponent;
2353
2590
  const finalMatchComponent = options.finalMatchComponent ?? matchComponent;
@@ -2357,32 +2594,33 @@ const generateBracketGridItems = (bracketData, roundTypeMap, swissGroups, roundR
2357
2594
  matchComponent: matchComponent,
2358
2595
  finalMatchComponent: finalMatchComponent,
2359
2596
  };
2597
+ const config = {
2598
+ bracketData,
2599
+ swissGroups,
2600
+ options: optionsWithDefaults,
2601
+ };
2360
2602
  switch (bracketData.mode) {
2361
2603
  case TOURNAMENT_MODE.DOUBLE_ELIMINATION:
2362
- return generateDoubleEliminationGridPlacements(bracketData, roundTypeMap, roundRelations, matchRelations, optionsWithDefaults);
2604
+ return generateDoubleEliminationGridPlacements(config);
2363
2605
  case TOURNAMENT_MODE.SWISS_WITH_ELIMINATION: {
2364
- if (!swissGroups)
2365
- throw new Error('Swiss groups are required for swiss with elimination mode');
2366
- return generateSwissWithEliminationGridPlacements(bracketData, roundTypeMap, swissGroups, roundRelations, matchRelations, optionsWithDefaults);
2606
+ return generateSwissWithEliminationGridPlacements(config);
2367
2607
  }
2368
2608
  default:
2369
- return generateGenericGridPlacements(bracketData, roundTypeMap, roundRelations, matchRelations, optionsWithDefaults);
2609
+ return generateGenericGridPlacements(config);
2370
2610
  }
2371
2611
  };
2372
- const generateGenericGridPlacements = (bracketData, roundTypeMap, roundRelations, matchRelations, options) => {
2612
+ const generateGenericGridPlacements = (config) => {
2373
2613
  const gridItems = new Map();
2374
- const firstRound = bracketData.rounds.values().next().value;
2614
+ const { bracketData, options } = config;
2615
+ const firstRound = bracketData.rounds.first();
2375
2616
  if (!firstRound) {
2376
2617
  throw new Error('First round is missing');
2377
2618
  }
2378
2619
  for (const round of bracketData.rounds.values()) {
2379
- const roundRelation = roundRelations.get(round.id);
2380
2620
  let currentMatchRow = 1;
2381
- const columnStart = round.index + 1;
2621
+ const columnStart = round.logicalIndex + 1;
2382
2622
  const columnEnd = columnStart + 1;
2383
- if (!roundRelation) {
2384
- throw new Error('Round relation is missing');
2385
- }
2623
+ const roundRelation = round.relation;
2386
2624
  if (roundRelation.type === 'two-to-nothing' || roundRelation.type === 'two-to-one') {
2387
2625
  throw new Error(`Invalid relation type ${roundRelation.type}`);
2388
2626
  }
@@ -2391,6 +2629,8 @@ const generateGenericGridPlacements = (bracketData, roundTypeMap, roundRelations
2391
2629
  layoutId: `${round.id}-layout`,
2392
2630
  columnStart,
2393
2631
  columnEnd,
2632
+ rowStart: 1,
2633
+ rowEnd: firstRound.matchCount + 1,
2394
2634
  roundRelation,
2395
2635
  items: new Map(),
2396
2636
  };
@@ -2413,10 +2653,7 @@ const generateGenericGridPlacements = (bracketData, roundTypeMap, roundRelations
2413
2653
  const rootRoundMatchFactor = roundRelation.type !== 'nothing-to-one' ? roundRelation.rootRoundMatchFactor : null;
2414
2654
  const matchHeight = rootRoundMatchFactor ? rootRoundMatchFactor : 1;
2415
2655
  for (const match of round.matches.values()) {
2416
- const matchRelation = matchRelations.get(match.id);
2417
- if (!matchRelation) {
2418
- throw new Error('Match relation is missing');
2419
- }
2656
+ const matchRelation = match.relation;
2420
2657
  const baseRow = match.indexInRound * matchHeight;
2421
2658
  const participantShortIds = [match.home?.shortId, match.away?.shortId].filter((id) => !!id).join(' ');
2422
2659
  const matchItem = {
@@ -2440,11 +2677,131 @@ const generateGenericGridPlacements = (bracketData, roundTypeMap, roundRelations
2440
2677
  }
2441
2678
  return gridItems;
2442
2679
  };
2443
- const generateDoubleEliminationGridPlacements = (bracketData, roundTypeMap, roundRelations, matchRelations, options) => {
2680
+ const generateDoubleEliminationGridPlacements = (config) => {
2444
2681
  const gridItems = new Map();
2682
+ const { bracketData, options } = config;
2683
+ const upperBracketRounds = bracketData.roundsByType.getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET);
2684
+ const lowerBracketRounds = bracketData.roundsByType.getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET);
2685
+ const hasReverseFinal = !!bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL);
2686
+ const firstUpperRound = upperBracketRounds.first();
2687
+ const firstLowerRound = lowerBracketRounds.first();
2688
+ const headerRowMod = options.includeRoundHeaders ? 1 : 0;
2689
+ if (!firstUpperRound) {
2690
+ throw new Error('First round is missing');
2691
+ }
2692
+ if (!firstLowerRound) {
2693
+ throw new Error('First lower round is missing');
2694
+ }
2695
+ const roundDelta = lowerBracketRounds.size - upperBracketRounds.size;
2696
+ const isAsyncBracket = roundDelta === 2;
2697
+ let currentUpperColumn = 1;
2698
+ for (const round of bracketData.rounds.values()) {
2699
+ let currentMatchRow = 1;
2700
+ const columnStart = round.logicalIndex + 1;
2701
+ const columnEnd = columnStart + 1;
2702
+ const roundRelation = round.relation;
2703
+ const isUpperBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET;
2704
+ const isUpperNeitherStartNorEnd = !round.isFirstOfType && !round.isLastOfType && isUpperBracket;
2705
+ const upperRowSpan = isUpperNeitherStartNorEnd || (isAsyncBracket && round.isLastOfType) ? 2 : 1;
2706
+ const isLowerBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
2707
+ const upperBracketRowStart = 1;
2708
+ const upperBracketRowEnd = firstUpperRound.matchCount + headerRowMod + 1;
2709
+ const lowerBracketRowStart = upperBracketRowEnd;
2710
+ const lowerBracketRowEnd = lowerBracketRowStart + headerRowMod + firstLowerRound.matchCount;
2711
+ const fullBracketRowStart = 1;
2712
+ const fullBracketRowEnd = lowerBracketRowEnd;
2713
+ const roundRowStart = isUpperBracket
2714
+ ? upperBracketRowStart
2715
+ : isLowerBracket
2716
+ ? lowerBracketRowStart
2717
+ : fullBracketRowStart;
2718
+ const roundRowEnd = isUpperBracket ? upperBracketRowEnd : isLowerBracket ? lowerBracketRowEnd : fullBracketRowEnd;
2719
+ const upperBracketColumnStart = currentUpperColumn;
2720
+ const upperBracketColumnEnd = currentUpperColumn + upperRowSpan;
2721
+ currentUpperColumn += upperRowSpan;
2722
+ const lowerBracketColumnStart = columnStart;
2723
+ const lowerBracketColumnEnd = columnEnd;
2724
+ const fullBracketColumnStart = columnStart;
2725
+ const fullBracketColumnEnd = columnEnd;
2726
+ const roundColumnStart = isUpperBracket
2727
+ ? upperBracketColumnStart
2728
+ : isLowerBracket
2729
+ ? lowerBracketColumnStart
2730
+ : fullBracketColumnStart;
2731
+ const roundColumnEnd = isUpperBracket
2732
+ ? upperBracketColumnEnd
2733
+ : isLowerBracket
2734
+ ? lowerBracketColumnEnd
2735
+ : fullBracketColumnEnd;
2736
+ const roundItem = {
2737
+ id: round.id,
2738
+ layoutId: `${round.id}-layout`,
2739
+ columnStart: roundColumnStart,
2740
+ columnEnd: roundColumnEnd,
2741
+ rowStart: roundRowStart,
2742
+ rowEnd: roundRowEnd,
2743
+ roundRelation,
2744
+ items: new Map(),
2745
+ };
2746
+ if (options.includeRoundHeaders) {
2747
+ const roundHeaderItem = {
2748
+ type: 'round',
2749
+ id: round.id,
2750
+ layoutId: `${round.id}-layout`,
2751
+ rowStart: currentMatchRow,
2752
+ rowEnd: ++currentMatchRow,
2753
+ component: options.headerComponent,
2754
+ className: 'et-bracket-new-item et-bracket-round-header-container',
2755
+ roundRelation,
2756
+ data: {
2757
+ bracketRound: round,
2758
+ },
2759
+ };
2760
+ roundItem.items.set(round.id, roundHeaderItem);
2761
+ }
2762
+ const rootRoundMatchFactor = roundRelation.type !== 'nothing-to-one' &&
2763
+ roundRelation.type !== 'two-to-nothing' &&
2764
+ roundRelation.type !== 'two-to-one'
2765
+ ? roundRelation.rootRoundMatchFactor
2766
+ : roundRelation.type === 'two-to-nothing' || roundRelation.type === 'two-to-one'
2767
+ ? roundRelation.upperRootRoundMatchFactor
2768
+ : null;
2769
+ const matchHeight = rootRoundMatchFactor ? rootRoundMatchFactor : 1;
2770
+ for (const match of round.matches.values()) {
2771
+ const matchRelation = match.relation;
2772
+ const baseRow = match.indexInRound * matchHeight;
2773
+ const participantShortIds = [match.home?.shortId, match.away?.shortId].filter((id) => !!id).join(' ');
2774
+ const matchItem = {
2775
+ type: 'match',
2776
+ id: match.id,
2777
+ layoutId: `${match.id}-layout`,
2778
+ rowStart: baseRow + currentMatchRow,
2779
+ rowEnd: baseRow + currentMatchRow + matchHeight,
2780
+ component: hasReverseFinal
2781
+ ? round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL
2782
+ ? options.finalMatchComponent
2783
+ : options.matchComponent
2784
+ : round.type === COMMON_BRACKET_ROUND_TYPE.FINAL
2785
+ ? options.finalMatchComponent
2786
+ : options.matchComponent,
2787
+ matchRelation,
2788
+ roundRelation,
2789
+ className: `et-bracket-new-item et-bracket-match-container ${participantShortIds}`,
2790
+ data: {
2791
+ bracketRound: round,
2792
+ bracketMatch: match,
2793
+ },
2794
+ };
2795
+ roundItem.items.set(match.id, matchItem);
2796
+ }
2797
+ gridItems.set(round.id, roundItem);
2798
+ }
2445
2799
  return gridItems;
2446
2800
  };
2447
- const generateSwissWithEliminationGridPlacements = (bracketData, roundTypeMap, swissGroups, roundRelations, matchRelations, options) => {
2801
+ const generateSwissWithEliminationGridPlacements = (config) => {
2802
+ const { swissGroups } = config;
2803
+ if (!swissGroups)
2804
+ throw new Error('Swiss groups are required for swiss with elimination mode');
2448
2805
  const gridItems = new Map();
2449
2806
  return gridItems;
2450
2807
  };
@@ -2468,17 +2825,16 @@ const createJourneyHighlight = (bracketData) => {
2468
2825
  };
2469
2826
 
2470
2827
  class NewBracketComponent {
2471
- #domSanitizer;
2472
- #elementId;
2473
2828
  constructor() {
2474
- this.#domSanitizer = inject(DomSanitizer);
2475
- this.#elementId = createComponentId('et-new-bracket');
2829
+ this.domSanitizer = inject(DomSanitizer);
2830
+ this.elementId = createComponentId('et-new-bracket');
2476
2831
  this.source = input.required();
2477
2832
  this.columnWidth = input(250, { transform: numberAttribute });
2478
2833
  this.matchHeight = input(75, { transform: numberAttribute });
2479
2834
  this.roundHeaderHeight = input(50, { transform: numberAttribute });
2480
2835
  this.columnGap = input(60, { transform: numberAttribute });
2481
2836
  this.rowGap = input(30, { transform: numberAttribute });
2837
+ this.upperLowerGap = input(50, { transform: numberAttribute });
2482
2838
  this.lineStartingCurveAmount = input(10, { transform: numberAttribute });
2483
2839
  this.lineEndingCurveAmount = input(0, { transform: numberAttribute });
2484
2840
  this.lineWidth = input(2, { transform: numberAttribute });
@@ -2490,50 +2846,59 @@ class NewBracketComponent {
2490
2846
  this.roundHeaderComponent = input();
2491
2847
  this.matchComponent = input();
2492
2848
  this.finalMatchComponent = input();
2493
- this.bracketData = computed(() => generateBracketData(this.source(), { layout: this.layout() }));
2494
- this.roundTypeMap = computed(() => generateBracketRoundTypeMap(this.bracketData()));
2495
- this.matchParticipantMap = computed(() => generateMatchParticipantMap(this.bracketData()));
2496
- this.matchPositionsMap = computed(() => generateMatchPositionMaps(this.bracketData()));
2497
- this.roundRelations = computed(() => generateRoundRelations(this.bracketData()));
2498
- this.matchRelations = computed(() => generateMatchRelations(this.bracketData(), this.roundRelations(), this.matchPositionsMap()));
2499
- this.swissGroups = computed(() => generateBracketRoundSwissGroupMaps(this.bracketData(), this.matchParticipantMap()));
2500
- this.items = computed(() => generateBracketGridItems(this.bracketData(), this.roundTypeMap(), this.swissGroups(), this.roundRelations(), this.matchRelations(), {
2501
- includeRoundHeaders: !this.hideRoundHeaders(),
2502
- headerComponent: this.roundHeaderComponent(),
2503
- matchComponent: this.matchComponent(),
2504
- finalMatchComponent: this.finalMatchComponent(),
2849
+ this.bracketData = computed(() => createNewBracket(this.source(), { layout: this.layout() }));
2850
+ this.swissGroups = computed(() => generateBracketRoundSwissGroupMaps(this.bracketData()));
2851
+ this.items = computed(() => generateBracketGridItems({
2852
+ bracketData: this.bracketData(),
2853
+ swissGroups: this.swissGroups(),
2854
+ options: {
2855
+ includeRoundHeaders: !this.hideRoundHeaders(),
2856
+ headerComponent: this.roundHeaderComponent(),
2857
+ matchComponent: this.matchComponent(),
2858
+ finalMatchComponent: this.finalMatchComponent(),
2859
+ },
2505
2860
  }));
2506
- this.definitions = computed(() => generateBracketGridDefinitions(this.bracketData(), this.roundTypeMap(), {
2861
+ this.definitions = computed(() => generateBracketGridDefinitions(this.bracketData(), {
2507
2862
  includeRoundHeaders: !this.hideRoundHeaders(),
2508
2863
  }));
2509
- this.firstRounds = computed(() => getFirstRounds(this.bracketData(), this.roundTypeMap()));
2510
- this.drawManData = computed(() => this.#domSanitizer.bypassSecurityTrustHtml(drawMan(this.items(), this.firstRounds(), {
2511
- columnGap: this.columnGap(),
2512
- columnWidth: this.columnWidth(),
2513
- matchHeight: this.matchHeight(),
2514
- roundHeaderHeight: this.hideRoundHeaders() ? 0 : this.roundHeaderHeight(),
2515
- rowGap: this.rowGap(),
2516
- gridDefinitions: this.definitions(),
2517
- curve: {
2518
- lineEndingCurveAmount: this.lineEndingCurveAmount(),
2519
- lineStartingCurveAmount: this.lineStartingCurveAmount(),
2520
- },
2521
- path: {
2522
- dashArray: this.lineDashArray(),
2523
- dashOffset: this.lineDashOffset(),
2524
- width: this.lineWidth(),
2525
- },
2526
- })));
2864
+ this.firstRounds = computed(() => getFirstRounds(this.bracketData()));
2865
+ this.drawManData = computed(() => {
2866
+ if (this.bracketData().mode !== TOURNAMENT_MODE.SINGLE_ELIMINATION)
2867
+ return '';
2868
+ return this.domSanitizer.bypassSecurityTrustHtml(drawMan(this.items(), this.firstRounds(), {
2869
+ columnGap: this.columnGap(),
2870
+ upperLowerGap: this.bracketData().mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION ? this.upperLowerGap() : 0,
2871
+ columnWidth: this.columnWidth(),
2872
+ matchHeight: this.matchHeight(),
2873
+ roundHeaderHeight: this.hideRoundHeaders() ? 0 : this.roundHeaderHeight(),
2874
+ rowGap: this.rowGap(),
2875
+ gridDefinitions: this.definitions(),
2876
+ curve: {
2877
+ lineEndingCurveAmount: this.lineEndingCurveAmount(),
2878
+ lineStartingCurveAmount: this.lineStartingCurveAmount(),
2879
+ },
2880
+ path: {
2881
+ dashArray: this.lineDashArray(),
2882
+ dashOffset: this.lineDashOffset(),
2883
+ width: this.lineWidth(),
2884
+ },
2885
+ }));
2886
+ });
2527
2887
  this.journeyHighlight = computed(() => this.disableJourneyHighlight() ? null : createJourneyHighlight(this.bracketData()));
2528
- const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2529
- if (!isBrowser)
2530
- return;
2888
+ this.setupJourneyHighlight();
2889
+ effect(() => {
2890
+ logRoundRelations(this.bracketData());
2891
+ console.log(this.bracketData());
2892
+ });
2893
+ }
2894
+ setupJourneyHighlight() {
2531
2895
  const renderer = inject(Renderer2);
2532
- const styleId = `et-new-bracket-journey-highlight--${this.#elementId}`;
2896
+ const doc = inject(DOCUMENT);
2897
+ const styleId = `et-new-bracket-journey-highlight--${this.elementId}`;
2533
2898
  let oldStyleEl = null;
2534
2899
  effect(() => {
2535
2900
  const newHighlightStyle = this.journeyHighlight();
2536
- const head = document.head;
2901
+ const head = doc.head;
2537
2902
  if (oldStyleEl) {
2538
2903
  renderer.removeChild(head, oldStyleEl);
2539
2904
  }
@@ -2547,31 +2912,31 @@ class NewBracketComponent {
2547
2912
  else {
2548
2913
  oldStyleEl = null;
2549
2914
  }
2550
- });
2551
- inject(DestroyRef).onDestroy(() => {
2552
- if (oldStyleEl) {
2553
- renderer.removeChild(document.head, oldStyleEl);
2554
- }
2915
+ return () => {
2916
+ if (oldStyleEl) {
2917
+ renderer.removeChild(head, oldStyleEl);
2918
+ oldStyleEl = null;
2919
+ }
2920
+ };
2555
2921
  });
2556
2922
  }
2557
2923
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2558
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: NewBracketComponent, isStandalone: true, selector: "et-new-bracket", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, columnWidth: { classPropertyName: "columnWidth", publicName: "columnWidth", isSignal: true, isRequired: false, transformFunction: null }, matchHeight: { classPropertyName: "matchHeight", publicName: "matchHeight", isSignal: true, isRequired: false, transformFunction: null }, roundHeaderHeight: { classPropertyName: "roundHeaderHeight", publicName: "roundHeaderHeight", isSignal: true, isRequired: false, transformFunction: null }, columnGap: { classPropertyName: "columnGap", publicName: "columnGap", isSignal: true, isRequired: false, transformFunction: null }, rowGap: { classPropertyName: "rowGap", publicName: "rowGap", isSignal: true, isRequired: false, transformFunction: null }, lineStartingCurveAmount: { classPropertyName: "lineStartingCurveAmount", publicName: "lineStartingCurveAmount", isSignal: true, isRequired: false, transformFunction: null }, lineEndingCurveAmount: { classPropertyName: "lineEndingCurveAmount", publicName: "lineEndingCurveAmount", isSignal: true, isRequired: false, transformFunction: null }, lineWidth: { classPropertyName: "lineWidth", publicName: "lineWidth", isSignal: true, isRequired: false, transformFunction: null }, lineDashArray: { classPropertyName: "lineDashArray", publicName: "lineDashArray", isSignal: true, isRequired: false, transformFunction: null }, lineDashOffset: { classPropertyName: "lineDashOffset", publicName: "lineDashOffset", isSignal: true, isRequired: false, transformFunction: null }, disableJourneyHighlight: { classPropertyName: "disableJourneyHighlight", publicName: "disableJourneyHighlight", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, hideRoundHeaders: { classPropertyName: "hideRoundHeaders", publicName: "hideRoundHeaders", isSignal: true, isRequired: false, transformFunction: null }, roundHeaderComponent: { classPropertyName: "roundHeaderComponent", publicName: "roundHeaderComponent", isSignal: true, isRequired: false, transformFunction: null }, matchComponent: { classPropertyName: "matchComponent", publicName: "matchComponent", isSignal: true, isRequired: false, transformFunction: null }, finalMatchComponent: { classPropertyName: "finalMatchComponent", publicName: "finalMatchComponent", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-new-bracket-host" }, ngImport: i0, template: "<section\n [style.--_cw.px]=\"columnWidth()\"\n [style.--_mh.px]=\"matchHeight()\"\n [style.--_rhh.px]=\"hideRoundHeaders() ? matchHeight() : roundHeaderHeight()\"\n [style.--_cg.px]=\"columnGap()\"\n [style.--_rg.px]=\"rowGap()\"\n [style.--_bcc]=\"definitions().columnCount\"\n [style.--_brc]=\"definitions().rowCount\"\n class=\"et-bracket-new\"\n>\n <svg [innerHTML]=\"drawManData()\" class=\"et-bracket-new-svg\" xmlns=\"http://www.w3.org/2000/svg\" />\n\n @for (round of items().values(); track round.layoutId) {\n <ul [style.--_c]=\"round.columnStart + ' / ' + round.columnEnd\" class=\"et-bracket-new-round\">\n @for (item of round.items.values(); track item.layoutId) {\n <li [style.--_r]=\"item.rowStart + ' / ' + item.rowEnd\" [class]=\"item.className\">\n <ng-container *ngComponentOutlet=\"item.component; inputs: item.data\" />\n </li>\n }\n </ul>\n }\n</section>\n", styles: [".et-bracket-new{display:grid;gap:var(--_cg);grid-auto-columns:var(--_cw);--bracket-line-color: red}.et-bracket-new-round{display:grid;grid-column:var(--_c);grid-row:1;list-style:none;margin:0;padding:0;gap:var(--_rg);grid-template-rows:var(--_rhh);grid-auto-rows:var(--_mh)}.et-bracket-new-item{grid-row:var(--_r);place-self:center;transition:opacity .2s}.et-bracket-new-svg{grid-column:1/calc(var(--_bcc) + 1);grid-row:1/2;inline-size:100%;block-size:100%}.et-bracket-new-svg path{color:var(--bracket-line-color);transition:opacity .2s}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
2924
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: NewBracketComponent, isStandalone: true, selector: "et-new-bracket", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, columnWidth: { classPropertyName: "columnWidth", publicName: "columnWidth", isSignal: true, isRequired: false, transformFunction: null }, matchHeight: { classPropertyName: "matchHeight", publicName: "matchHeight", isSignal: true, isRequired: false, transformFunction: null }, roundHeaderHeight: { classPropertyName: "roundHeaderHeight", publicName: "roundHeaderHeight", isSignal: true, isRequired: false, transformFunction: null }, columnGap: { classPropertyName: "columnGap", publicName: "columnGap", isSignal: true, isRequired: false, transformFunction: null }, rowGap: { classPropertyName: "rowGap", publicName: "rowGap", isSignal: true, isRequired: false, transformFunction: null }, upperLowerGap: { classPropertyName: "upperLowerGap", publicName: "upperLowerGap", isSignal: true, isRequired: false, transformFunction: null }, lineStartingCurveAmount: { classPropertyName: "lineStartingCurveAmount", publicName: "lineStartingCurveAmount", isSignal: true, isRequired: false, transformFunction: null }, lineEndingCurveAmount: { classPropertyName: "lineEndingCurveAmount", publicName: "lineEndingCurveAmount", isSignal: true, isRequired: false, transformFunction: null }, lineWidth: { classPropertyName: "lineWidth", publicName: "lineWidth", isSignal: true, isRequired: false, transformFunction: null }, lineDashArray: { classPropertyName: "lineDashArray", publicName: "lineDashArray", isSignal: true, isRequired: false, transformFunction: null }, lineDashOffset: { classPropertyName: "lineDashOffset", publicName: "lineDashOffset", isSignal: true, isRequired: false, transformFunction: null }, disableJourneyHighlight: { classPropertyName: "disableJourneyHighlight", publicName: "disableJourneyHighlight", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, hideRoundHeaders: { classPropertyName: "hideRoundHeaders", publicName: "hideRoundHeaders", isSignal: true, isRequired: false, transformFunction: null }, roundHeaderComponent: { classPropertyName: "roundHeaderComponent", publicName: "roundHeaderComponent", isSignal: true, isRequired: false, transformFunction: null }, matchComponent: { classPropertyName: "matchComponent", publicName: "matchComponent", isSignal: true, isRequired: false, transformFunction: null }, finalMatchComponent: { classPropertyName: "finalMatchComponent", publicName: "finalMatchComponent", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-new-bracket-host" }, ngImport: i0, template: "<section\n [style.--_cw.px]=\"columnWidth()\"\n [style.--_mh.px]=\"matchHeight()\"\n [style.--_rhh.px]=\"hideRoundHeaders() ? matchHeight() : roundHeaderHeight()\"\n [style.--_cg.px]=\"columnGap()\"\n [style.--_ulg.px]=\"upperLowerGap()\"\n [style.--_rg.px]=\"rowGap()\"\n [style.--_bcc]=\"definitions().columnCount\"\n [style.--_brc]=\"definitions().rowCount\"\n class=\"et-bracket-new\"\n>\n <svg [innerHTML]=\"drawManData()\" class=\"et-bracket-new-svg\" xmlns=\"http://www.w3.org/2000/svg\" />\n\n @for (round of items().values(); track round.layoutId) {\n <ul\n [style.--_c]=\"round.columnStart + ' / ' + round.columnEnd\"\n [style.--_r]=\"round.rowStart + ' / ' + round.rowEnd\"\n class=\"et-bracket-new-round\"\n >\n @for (item of round.items.values(); track item.layoutId) {\n <li [style.--_r]=\"item.rowStart + ' / ' + item.rowEnd\" [class]=\"item.className\">\n <ng-container *ngComponentOutlet=\"item.component; inputs: item.data\" />\n </li>\n }\n </ul>\n }\n</section>\n", styles: [".et-bracket-new{display:grid;column-gap:var(--_cg);row-gap:var(--_ulg);grid-auto-columns:var(--_cw);--bracket-line-color: red}.et-bracket-new-round{display:grid;grid-column:var(--_c);grid-row:var(--_r);list-style:none;margin:0;padding:0;row-gap:var(--_rg);grid-template-rows:var(--_rhh);grid-auto-rows:var(--_mh)}.et-bracket-new-item{grid-row:var(--_r);place-self:center;transition:opacity .2s}.et-bracket-new-svg{grid-column:1/calc(var(--_bcc) + 1);grid-row:1/var(--_brc);inline-size:100%;block-size:100%}.et-bracket-new-svg path{color:var(--bracket-line-color);transition:opacity .2s}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
2559
2925
  }
2560
2926
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NewBracketComponent, decorators: [{
2561
2927
  type: Component,
2562
2928
  args: [{ selector: 'et-new-bracket', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
2563
2929
  class: 'et-new-bracket-host',
2564
- }, imports: [NgComponentOutlet], template: "<section\n [style.--_cw.px]=\"columnWidth()\"\n [style.--_mh.px]=\"matchHeight()\"\n [style.--_rhh.px]=\"hideRoundHeaders() ? matchHeight() : roundHeaderHeight()\"\n [style.--_cg.px]=\"columnGap()\"\n [style.--_rg.px]=\"rowGap()\"\n [style.--_bcc]=\"definitions().columnCount\"\n [style.--_brc]=\"definitions().rowCount\"\n class=\"et-bracket-new\"\n>\n <svg [innerHTML]=\"drawManData()\" class=\"et-bracket-new-svg\" xmlns=\"http://www.w3.org/2000/svg\" />\n\n @for (round of items().values(); track round.layoutId) {\n <ul [style.--_c]=\"round.columnStart + ' / ' + round.columnEnd\" class=\"et-bracket-new-round\">\n @for (item of round.items.values(); track item.layoutId) {\n <li [style.--_r]=\"item.rowStart + ' / ' + item.rowEnd\" [class]=\"item.className\">\n <ng-container *ngComponentOutlet=\"item.component; inputs: item.data\" />\n </li>\n }\n </ul>\n }\n</section>\n", styles: [".et-bracket-new{display:grid;gap:var(--_cg);grid-auto-columns:var(--_cw);--bracket-line-color: red}.et-bracket-new-round{display:grid;grid-column:var(--_c);grid-row:1;list-style:none;margin:0;padding:0;gap:var(--_rg);grid-template-rows:var(--_rhh);grid-auto-rows:var(--_mh)}.et-bracket-new-item{grid-row:var(--_r);place-self:center;transition:opacity .2s}.et-bracket-new-svg{grid-column:1/calc(var(--_bcc) + 1);grid-row:1/2;inline-size:100%;block-size:100%}.et-bracket-new-svg path{color:var(--bracket-line-color);transition:opacity .2s}\n"] }]
2930
+ }, imports: [NgComponentOutlet], template: "<section\n [style.--_cw.px]=\"columnWidth()\"\n [style.--_mh.px]=\"matchHeight()\"\n [style.--_rhh.px]=\"hideRoundHeaders() ? matchHeight() : roundHeaderHeight()\"\n [style.--_cg.px]=\"columnGap()\"\n [style.--_ulg.px]=\"upperLowerGap()\"\n [style.--_rg.px]=\"rowGap()\"\n [style.--_bcc]=\"definitions().columnCount\"\n [style.--_brc]=\"definitions().rowCount\"\n class=\"et-bracket-new\"\n>\n <svg [innerHTML]=\"drawManData()\" class=\"et-bracket-new-svg\" xmlns=\"http://www.w3.org/2000/svg\" />\n\n @for (round of items().values(); track round.layoutId) {\n <ul\n [style.--_c]=\"round.columnStart + ' / ' + round.columnEnd\"\n [style.--_r]=\"round.rowStart + ' / ' + round.rowEnd\"\n class=\"et-bracket-new-round\"\n >\n @for (item of round.items.values(); track item.layoutId) {\n <li [style.--_r]=\"item.rowStart + ' / ' + item.rowEnd\" [class]=\"item.className\">\n <ng-container *ngComponentOutlet=\"item.component; inputs: item.data\" />\n </li>\n }\n </ul>\n }\n</section>\n", styles: [".et-bracket-new{display:grid;column-gap:var(--_cg);row-gap:var(--_ulg);grid-auto-columns:var(--_cw);--bracket-line-color: red}.et-bracket-new-round{display:grid;grid-column:var(--_c);grid-row:var(--_r);list-style:none;margin:0;padding:0;row-gap:var(--_rg);grid-template-rows:var(--_rhh);grid-auto-rows:var(--_mh)}.et-bracket-new-item{grid-row:var(--_r);place-self:center;transition:opacity .2s}.et-bracket-new-svg{grid-column:1/calc(var(--_bcc) + 1);grid-row:1/var(--_brc);inline-size:100%;block-size:100%}.et-bracket-new-svg path{color:var(--bracket-line-color);transition:opacity .2s}\n"] }]
2565
2931
  }], ctorParameters: () => [] });
2566
2932
 
2567
2933
  var index = /*#__PURE__*/Object.freeze({
2568
2934
  __proto__: null,
2569
2935
  BRACKET_DATA_LAYOUT: BRACKET_DATA_LAYOUT,
2570
2936
  BRACKET_ROUND_MIRROR_TYPE: BRACKET_ROUND_MIRROR_TYPE,
2937
+ BracketMap: BracketMap,
2571
2938
  COMMON_BRACKET_ROUND_TYPE: COMMON_BRACKET_ROUND_TYPE,
2572
2939
  DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE: DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE,
2573
- FALLBACK_MATCH_POSITION: FALLBACK_MATCH_POSITION,
2574
- FIRST_ROUNDS_TYPE: FIRST_ROUNDS_TYPE,
2575
2940
  GROUP_BRACKET_ROUND_TYPE: GROUP_BRACKET_ROUND_TYPE,
2576
2941
  NewBracketComponent: NewBracketComponent,
2577
2942
  NewBracketDefaultMatchComponent: NewBracketDefaultMatchComponent,
@@ -2580,27 +2945,17 @@ var index = /*#__PURE__*/Object.freeze({
2580
2945
  SWISS_BRACKET_ROUND_TYPE: SWISS_BRACKET_ROUND_TYPE,
2581
2946
  TOURNAMENT_MODE: TOURNAMENT_MODE,
2582
2947
  canRenderLayoutInTournamentMode: canRenderLayoutInTournamentMode,
2583
- drawMan: drawMan,
2584
- generateBracketData: generateBracketData,
2948
+ createMatchesMapBase: createMatchesMapBase,
2949
+ createNewBracketBase: createNewBracketBase,
2950
+ createNewMatchParticipantBase: createNewMatchParticipantBase,
2951
+ createParticipantsMapBase: createParticipantsMapBase,
2952
+ createRoundsMapBase: createRoundsMapBase,
2585
2953
  generateBracketDataForEthlete: generateBracketDataForEthlete,
2586
2954
  generateBracketDataForGg: generateBracketDataForGg,
2587
- generateBracketGridDefinitions: generateBracketGridDefinitions,
2588
- generateBracketGridItems: generateBracketGridItems,
2589
- generateBracketRoundSwissGroupMaps: generateBracketRoundSwissGroupMaps,
2590
- generateBracketRoundTypeMap: generateBracketRoundTypeMap,
2591
- generateMatchParticipantMap: generateMatchParticipantMap,
2592
- generateMatchPosition: generateMatchPosition,
2593
- generateMatchPositionMaps: generateMatchPositionMaps,
2594
- generateMatchRelationPositions: generateMatchRelationPositions,
2595
- generateMatchRelations: generateMatchRelations,
2596
- generateRoundRelations: generateRoundRelations,
2597
2955
  generateRoundTypeFromEthleteRoundType: generateRoundTypeFromEthleteRoundType,
2598
2956
  generateRoundTypeFromGgMatch: generateRoundTypeFromGgMatch,
2599
2957
  generateTournamentModeFormEthleteRounds: generateTournamentModeFormEthleteRounds,
2600
- generateTournamentModeFormGgData: generateTournamentModeFormGgData,
2601
- getAvailableSwissGroupsForRound: getAvailableSwissGroupsForRound,
2602
- getFirstRounds: getFirstRounds,
2603
- logRoundRelations: logRoundRelations
2958
+ generateTournamentModeFormGgData: generateTournamentModeFormGgData
2604
2959
  });
2605
2960
 
2606
2961
  class ButtonDirective {
@@ -8902,7 +9257,7 @@ class SliderComponent {
8902
9257
  constructor() {
8903
9258
  this._elementRef = inject(ElementRef);
8904
9259
  this._dirService = inject(Directionality);
8905
- this._document = inject(DOCUMENT);
9260
+ this._document = inject(DOCUMENT$1);
8906
9261
  this._destroy$ = createDestroy();
8907
9262
  this._mouseDown$ = fromEvent(this._elementRef.nativeElement, 'mousedown', { passive: false });
8908
9263
  this._touchStart$ = fromEvent(this._elementRef.nativeElement, 'touchstart', {
@@ -9589,7 +9944,7 @@ class BottomSheetContainerBaseComponent extends CdkDialogContainer {
9589
9944
  this._trapFocus();
9590
9945
  }
9591
9946
  }
9592
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: BottomSheetContainerBaseComponent, deps: [{ token: i0.ElementRef }, { token: i1$1.FocusTrapFactory }, { token: DOCUMENT, optional: true }, { token: BOTTOM_SHEET_CONFIG }, { token: i1$1.InteractivityChecker }, { token: i0.NgZone }, { token: i2$1.OverlayRef }, { token: i1$1.FocusMonitor }], target: i0.ɵɵFactoryTarget.Component }); }
9947
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: BottomSheetContainerBaseComponent, deps: [{ token: i0.ElementRef }, { token: i1$1.FocusTrapFactory }, { token: DOCUMENT$1, optional: true }, { token: BOTTOM_SHEET_CONFIG }, { token: i1$1.InteractivityChecker }, { token: i0.NgZone }, { token: i2$1.OverlayRef }, { token: i1$1.FocusMonitor }], target: i0.ɵɵFactoryTarget.Component }); }
9593
9948
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.3", type: BottomSheetContainerBaseComponent, isStandalone: true, selector: "et-bottom-sheet-container-base", usesInheritance: true, ngImport: i0, template: '', isInline: true }); }
9594
9949
  }
9595
9950
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: BottomSheetContainerBaseComponent, decorators: [{
@@ -9602,7 +9957,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImpor
9602
9957
  type: Optional
9603
9958
  }, {
9604
9959
  type: Inject,
9605
- args: [DOCUMENT]
9960
+ args: [DOCUMENT$1]
9606
9961
  }] }, { type: undefined, decorators: [{
9607
9962
  type: Inject,
9608
9963
  args: [BOTTOM_SHEET_CONFIG]
@@ -9619,7 +9974,7 @@ class BottomSheetContainerComponent extends BottomSheetContainerBaseComponent {
9619
9974
  constructor() {
9620
9975
  const elementRef = inject(ElementRef);
9621
9976
  const focusTrapFactory = inject(FocusTrapFactory);
9622
- const document = inject(DOCUMENT);
9977
+ const document = inject(DOCUMENT$1);
9623
9978
  const bottomSheetConfig = inject(BOTTOM_SHEET_CONFIG);
9624
9979
  const checker = inject(InteractivityChecker);
9625
9980
  const ngZone = inject(NgZone);
@@ -10290,7 +10645,7 @@ class DialogContainerBaseComponent extends CdkDialogContainer {
10290
10645
  this._trapFocus();
10291
10646
  }
10292
10647
  }
10293
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: DialogContainerBaseComponent, deps: [{ token: i0.ElementRef }, { token: i1$1.FocusTrapFactory }, { token: DOCUMENT, optional: true }, { token: DIALOG_CONFIG }, { token: i1$1.InteractivityChecker }, { token: i0.NgZone }, { token: i2$1.OverlayRef }, { token: i1$1.FocusMonitor }], target: i0.ɵɵFactoryTarget.Component }); }
10648
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: DialogContainerBaseComponent, deps: [{ token: i0.ElementRef }, { token: i1$1.FocusTrapFactory }, { token: DOCUMENT$1, optional: true }, { token: DIALOG_CONFIG }, { token: i1$1.InteractivityChecker }, { token: i0.NgZone }, { token: i2$1.OverlayRef }, { token: i1$1.FocusMonitor }], target: i0.ɵɵFactoryTarget.Component }); }
10294
10649
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.3", type: DialogContainerBaseComponent, isStandalone: true, selector: "et-dialog-container-base", usesInheritance: true, ngImport: i0, template: '', isInline: true }); }
10295
10650
  }
10296
10651
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: DialogContainerBaseComponent, decorators: [{
@@ -10303,7 +10658,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImpor
10303
10658
  type: Optional
10304
10659
  }, {
10305
10660
  type: Inject,
10306
- args: [DOCUMENT]
10661
+ args: [DOCUMENT$1]
10307
10662
  }] }, { type: undefined, decorators: [{
10308
10663
  type: Inject,
10309
10664
  args: [DIALOG_CONFIG]
@@ -10320,7 +10675,7 @@ class DialogContainerComponent extends DialogContainerBaseComponent {
10320
10675
  constructor() {
10321
10676
  const elementRef = inject(ElementRef);
10322
10677
  const focusTrapFactory = inject(FocusTrapFactory);
10323
- const document = inject(DOCUMENT);
10678
+ const document = inject(DOCUMENT$1);
10324
10679
  const dialogConfig = inject(DIALOG_CONFIG);
10325
10680
  const checker = inject(InteractivityChecker);
10326
10681
  const ngZone = inject(NgZone);
@@ -13661,7 +14016,7 @@ class OverlayContainerComponent extends CdkDialogContainer {
13661
14016
  return super._ariaLabelledBy;
13662
14017
  }
13663
14018
  constructor() {
13664
- super(inject(ElementRef), inject(FocusTrapFactory), inject(DOCUMENT), inject(OVERLAY_CONFIG), inject(InteractivityChecker), inject(NgZone), inject(OverlayRef$1), inject(FocusMonitor));
14019
+ super(inject(ElementRef), inject(FocusTrapFactory), inject(DOCUMENT$1), inject(OVERLAY_CONFIG), inject(InteractivityChecker), inject(NgZone), inject(OverlayRef$1), inject(FocusMonitor));
13665
14020
  this._swipeHandlerService = inject(SwipeHandlerService);
13666
14021
  this._dragToDismissStop$ = new Subject();
13667
14022
  this._themeProvider = inject(THEME_PROVIDER);
@@ -16528,7 +16883,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImpor
16528
16883
  class PaginationHeadService {
16529
16884
  constructor() {
16530
16885
  this._config = null;
16531
- this._document = inject(DOCUMENT);
16886
+ this._document = inject(DOCUMENT$1);
16532
16887
  this._titleService = inject(Title);
16533
16888
  this._router = inject(Router);
16534
16889
  this._head = this._document.getElementsByTagName('head')[0];
@@ -18411,7 +18766,7 @@ class InlineTabBodyHostDirective extends CdkPortalOutlet {
18411
18766
  constructor() {
18412
18767
  const componentFactoryResolver = inject(ComponentFactoryResolver);
18413
18768
  const viewContainerRef = inject(ViewContainerRef);
18414
- const _document = inject(DOCUMENT);
18769
+ const _document = inject(DOCUMENT$1);
18415
18770
  super(componentFactoryResolver, viewContainerRef, _document);
18416
18771
  }
18417
18772
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InlineTabBodyHostDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }