Skip to content

When drag/dropping the scrollbar, rxVirtualViewContent tends to go a bit crazy #1868

@LanderBeeuwsaert

Description

@LanderBeeuwsaert

Working further after I got feedback on my stupid mistake from this:
#1864

The directives seem to work. Really nice!
Except for the following, see video:
https://www.loom.com/share/97fb2f0d78344ebf848eff2d467edd0c
so when drag/dropping the scrollbar, it's (very) easy to get a sort of recursive/repetitive jumping after dropping the scrollbar

The code:

import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { Discipline, DisciplineType } from '../../../shared/models/discipline.model';
import { AngularDisciplineService } from '../core/services/discipline.service';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { CollaborationsInformationService } from './collaborations-information.service';
import { BehaviorSubject, combineLatest, filter, firstValueFrom, lastValueFrom, map, Observable, startWith, tap } from 'rxjs';
import { CollaborationCompetitionProjection, CompetitionType, Organisation } from '../../../shared/models/organisation.model';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { LocalStorage } from '../core/services/local-storage';
import { doesFormHaveErrors } from '../core/services/get-form-error-texts.service';
import { NavigationSidebarService } from '../sidebar/navigation-sidebar/navigation-sidebar.service';
import { OrganisationProjectionService } from '../core/services/organisation-projection.service';
import { transition, trigger, useAnimation } from '@angular/animations';
import { fadeIn, fadeOut } from '../core/animations/fade-in-out';
import { DateUtils } from '../../../shared/date.utils';
import { Helpers } from 'app/core/services/helpers.service';
import { UserService } from 'app/core/services/data-model-services/user.service';
import Fuse from 'fuse.js';
import { AngularCountryService } from 'app/core/services/countries.service';
import { AngularLevelHelpersService } from 'app/core/services/level-helpers.service';
import { CurrentOrganisationService } from 'app/core/services/organisation-service/current-organisation.service';
import { getMonth } from 'date-fns';
import { AuthenticationService } from 'app/core/services/authentication.service';
import { ToastService } from 'app/generic-components/toast/toast.service';
import { OrganisationService } from 'app/core/services/data-model-services/organisation.service';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { DropDownDialogData, DropDownDialogOption, DropdownDialogComponent } from 'app/generic-components/dropdown-dialog/dropdown-dialog.component';
import { Timestamp } from 'firebase/firestore';
import { LoadingOverlayService } from 'app/core/services/loading-overlay.service';
import { AsyncPipe } from '@angular/common';
import { CountrySelectComponent } from '../generic-components/country-select/country-select.component';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { MatExpansionPanel, MatExpansionPanelHeader } from '@angular/material/expansion';
import { PictureSwitchAnimationComponent } from '../generic-components/picture-switch-animation/picture-switch-animation.component';
import { MatTooltip } from '@angular/material/tooltip';
import { MatIcon } from '@angular/material/icon';
import { MatButton } from '@angular/material/button';
import { FooterComponent } from '../footer/footer.component';
import { MatInput } from '@angular/material/input';
import { RxVirtualView, RxVirtualViewContent, RxVirtualViewObserver, RxVirtualViewPlaceholder } from '@rx-angular/template/virtual-view';

@UntilDestroy()
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'collaboration-information-main',
  styleUrls: ['collaborations-information-main.style.scss'],
  template: `
    <!--HEADER-->
    @let competitions = competitions$ | async;
    @let showNavigationSidebar = showNavigationSidebar$ | async;

    <div rxVirtualViewObserver style="overflow-y: auto;">
      <div [class.sidebar-open-header-container]="showNavigationSidebar" class="header-background header-container">
        <div class="header-background-rotate header">
          <!--TITLES-->
          <div class="titles-container">
            <h1 class="title">Collaborations</h1>
            <p class="subtitle">Explore our collaborations!</p>
          </div>
          <!--FILTERS-->
          <div [class.sidebar-open-filters-container]="showNavigationSidebar" class="filters-container">
            @if (collaborationsInformationService.countryList$ | async; as countryList) {
              <div
                [@fadeInOut]
                [class.sidebar-open-filters-select-box-container]="showNavigationSidebar"
                class="filters-select-box-container"
                [formGroup]="filtersForm"
              >
                <div [class.sidebar-open-filter-select-box]="showNavigationSidebar" class="filter-select-box">
                  <p class="filter-label">Country</p>
                  <country-select
                    appearance="outline"
                    [countriesToShow]="countryList"
                    [form]="filtersForm"
                    [shouldHaveEmptySelection]="true"
                    [shouldShowLabel]="false"
                    property="country"
                  ></country-select>
                </div>
                <div>
                  <p class="filter-label">Competition type</p>
                  <mat-form-field [class.sidebar-open-filter-select-box]="showNavigationSidebar" class="filter-select-box" appearance="outline">
                    <mat-select matInput placeholder="Competition Type" formControlName="competition_type" (focusout)="cdr.markForCheck()">
                      <mat-option></mat-option>
                      @for (type of competitionTypes; track type) {
                        <mat-option color="primary" [value]="type.toLowerCase()">{{ type }}</mat-option>
                      }
                    </mat-select>
                  </mat-form-field>
                </div>
                <div>
                  <p class="filter-label">Competition discipline</p>
                  <mat-form-field [class.sidebar-open-filter-select-box]="showNavigationSidebar" class="filter-select-box" appearance="outline">
                    <mat-select matInput placeholder="Competition discipline" formControlName="discipline" (focusout)="cdr.markForCheck()">
                      <mat-option></mat-option>
                      @for (discipline of disciplines; track discipline.type) {
                        <mat-option [value]="discipline.type">{{ discipline.translationLabel | translate }}</mat-option>
                      }
                    </mat-select>
                  </mat-form-field>
                </div>
                <div>
                  <p class="filter-label">Year</p>
                  <mat-form-field [class.sidebar-open-filter-select-box]="showNavigationSidebar" class="filter-select-box" appearance="outline">
                    <mat-select matInput placeholder="Year" formControlName="competitionYear" (focusout)="cdr.markForCheck()">
                      <mat-option></mat-option>
                      <mat-option value="upcoming">Upcoming</mat-option>
                      @for (year of competitionYears; track year) {
                        <mat-option [value]="year">{{ year }}</mat-option>
                      }
                    </mat-select>
                  </mat-form-field>
                </div>
                <div>
                  <p class="filter-label">Search for competition</p>
                  <mat-form-field [class.sidebar-open-filter-select-box]="showNavigationSidebar" class="filter-select-box" appearance="outline">
                    <input matInput formControlName="freeTextFilter" />
                  </mat-form-field>
                </div>
              </div>
            }
          </div>
        </div>
      </div>
      <div class="divider"></div>
      <div [class.sidebar-open-content-container]="showNavigationSidebar$ | async" class="content-container white-space">
        <p class="results-text">Showing {{ amountOfResultsFetched }} Results</p>
        @if (competitions?.length) {
          <div class="card-container flex-column">
            @for (competition of competitions; track competition.key; let i = $index) {
              <div rxVirtualView class="testing">
              @if (isNewMonth(competitions[i === 0 ? 0 : i - 1].startDateSecondsAsNumber, competition.startDateSecondsAsNumber, i === 0)) {
                <div class="month-container">
                  <div class="month-info">{{ getMonthAndYearForDisplay(competition.startDateSecondsAsNumber) }}</div>
                </div>
              }
              <mat-expansion-panel
                *rxVirtualViewContenthttps://github.com/rx-angular/rx-angular/issues/1864
                hideToggle
                class="card"
                [attr.data-e2e-id]="competition.key"
                [class.disabled-card]="competition.isCompetitionCancelled"
              >
                <mat-expansion-panel-header [collapsedHeight]="'100%'" [expandedHeight]="'100%'" (click)="togglePanelState(competition)">
                  <picture-switch-animation
                    id="card-image"
                    [newPicturePath]="getCompImage(competition)"
                    [pictureWidth]="'100%'"
                    [isSquare]="true"
                    [class.competition-is-cancelled]="competition.isCompetitionCancelled"
                  ></picture-switch-animation>
                  <div id="card-text" class="flex-row">
                    <div class="left-section flex-column ">
                      <div id="competition-info" class="flex-column">
                        <div id="cancelled-and-location" class="flex-row">
                          @if (competition.isCompetitionCancelled) {
                            <p id="cancelled-text">Cancelled</p>
                          }
                          <div class="country-flag flex-row" [class.competition-is-cancelled]="competition.isCompetitionCancelled">
                            <div [class]="'flag-icon flag-icon-' + countryService.getCountryShort2(competition.country)"></div>
                            <div>
                              {{ countryService.getCountryName(competition.country) + (competition?.city ? ' - ' + competition?.city : '') }}
                            </div>
                          </div>
                        </div>
                        <div class="add-bold" [class.competition-is-cancelled]="competition.isCompetitionCancelled">{{ competition.name }}</div>
                        <div [class.competition-is-cancelled]="competition.isCompetitionCancelled">
                          {{ competition.startDateString }} - {{ competition.endDateString }}
                        </div>
                      </div>
                      <p
                        id="follow-the-competition"
                        class="follow-competition"
                        [class.competition-is-cancelled]="competition.isCompetitionCancelled"
                        (click)="navigateToLiveCompetition(competition.key)"
                        [matTooltip]="'Navigate to ' + competition.name + ' live competition view'"
                        [matTooltipPosition]="'below'"
                      >
                        <mat-icon>open_in_new</mat-icon>
                        Follow the competition
                      </p>
                    </div>

                    <div class="right-section flex-column" [class.competition-is-cancelled]="competition.isCompetitionCancelled">
                      @if (competition.has_registrations || competition.is_registration_enabled) {
                        <div class="align-right flex-row" [matTooltip]="getRegistrationTootipButtonText(competition)">
                          <button
                            id="registration-button"
                            [disabled]="isRegistrationButtonDisabled(competition)"
                            (click)="navigateToRegistration($event, competition)"
                            mat-raised-button
                            color="primary"
                          >
                            Register
                          </button>
                        </div>
                      }
                      <div id="open-card-button" class="align-right flex-row">
                        @if (!competition.panelStateOpen) {
                          <div class="add-bold">More Info</div>
                        } @else {
                          <div class="add-bold">Hide Info</div>
                        }
                        <mat-icon [class.transform-open-close]="competition.panelStateOpen === true">expand_more </mat-icon>
                      </div>
                    </div>
                  </div>
                </mat-expansion-panel-header>

                <div class="expansion-panel-divider"></div>
                <div id="open-card-container" class="flex-column" [class.competition-is-cancelled]="competition.isCompetitionCancelled">
                  <div class="add-bold">Competition levels</div>
                  <ul class="comp-levels flex-row">
                    @if (competition.levelsProjection) {
                      @for (level of competition.levelsProjection; track level; let i = $index) {
                        <li>{{ levelHelpersService.getLevelNameForDisplayFromLongLevel(competition.discipline, level) }}</li>
                      }
                    } @else {
                      -
                    }
                  </ul>
                </div>
              </mat-expansion-panel>
              <div *rxVirtualViewPlaceholder style="height: 500px;">test</div>
              </div>
            }
          </div>
        }
        @if (areThereMoreThanThirtyCompetitions) {
          <button id="load-all-competitions-button" mat-raised-button color="primary" (click)="shouldLoadAllCompetitions$$.next(true)">
            <p>Load All Competitions</p>
            <mat-icon>cached</mat-icon>
          </button>
        }
      </div>
      <footer-main></footer-main>
    </div>
  `,
  animations: [
    trigger('fadeInOut', [
      transition('void => *', [useAnimation(fadeIn, { params: { timings: '300ms ease-out' } })]),
      transition('* => void', [useAnimation(fadeOut, { params: { timings: '300ms ease-out' } })]),
      transition('* => void', [useAnimation(fadeOut, { params: { params: '500ms ease-out' } })]),
    ]),
  ],
  imports: [
    AsyncPipe,
    ReactiveFormsModule,
    CountrySelectComponent,
    MatFormField,
    MatSelect,
    MatOption,
    TranslateModule,
    MatExpansionPanel,
    PictureSwitchAnimationComponent,
    MatExpansionPanelHeader,
    MatTooltip,
    MatIcon,
    MatButton,
    FooterComponent,
    MatInput,
    RxVirtualViewObserver,
    RxVirtualView,
    RxVirtualViewContent,
    RxVirtualViewPlaceholder,
  ],
})
export class CollaborationsInformationMainComponent {
  disciplines: readonly Discipline[];
  competitionYears: string[];
  public filtersForm: FormGroup;
  public currentQueryParams;
  competitionTypes: string[] = ['National', 'International', 'Official'];

  competitions$: Observable<CollaborationCompetitionProjection[]>;

  amountOfResultsFetched: number;

  areThereMoreThanThirtyCompetitions: boolean;

  protected shouldLoadAllCompetitions$$: BehaviorSubject<boolean> = new BehaviorSubject(false);
  shouldLoadAllCompetitions$: Observable<boolean> = this.shouldLoadAllCompetitions$$.asObservable();

  readonly showNavigationSidebar$: Observable<boolean>;

  readonly collaborationCountryKey: string = 'collaboration_country_filter';
  readonly collaborationDisciplineKey: string = 'collaboration_discipline_filter';
  readonly collaborationTypeKey: string = 'collaboration_competition_type_filter';
  readonly collaborationYearKey: string = 'collaboration_competition_year_filter';

  constructor(
    public disciplineService: AngularDisciplineService,
    public collaborationsInformationService: CollaborationsInformationService,
    private fb: FormBuilder,
    private titleService: Title,
    private route: ActivatedRoute,
    private helpers: Helpers,
    private router: Router,
    private dialog: MatDialog,
    private localStorage: LocalStorage,
    private navigationSidebarService: NavigationSidebarService,
    private organisationProjectionService: OrganisationProjectionService,
    public cdr: ChangeDetectorRef,
    public authenticationService: AuthenticationService,
    private userService: UserService,
    public countryService: AngularCountryService,
    public levelHelpersService: AngularLevelHelpersService,
    public currentOrganisationService: CurrentOrganisationService,
    private toastService: ToastService,
    private organisationService: OrganisationService,
    private loadingOverlayService: LoadingOverlayService,
  ) {
    this.disciplines = this.disciplineService.getDisciplines();
    this.showNavigationSidebar$ = this.navigationSidebarService.showNavigationSidebar$;
    this.loadingOverlayService.enable('load-collaborations', 'Loading collaborations...');
    this.collaborationsInformationService.loadCompetitionsToShow();

    this.createForm();
    this.currentQueryParams = { ...this.route.snapshot.queryParams };
    if (this.currentQueryParams.country) {
      this.filtersForm.get('country').patchValue(this.currentQueryParams.country);
    } else if (!!this.getFilterProperties(this.collaborationCountryKey)) {
      this.filtersForm.get('country').patchValue(this.getFilterProperties(this.collaborationCountryKey));
    }
    if (this.currentQueryParams.discipline) {
      this.filtersForm.get('discipline').patchValue(this.currentQueryParams.discipline);
    } else if (!!this.getFilterProperties(this.collaborationDisciplineKey)) {
      this.filtersForm.get('discipline').patchValue(this.getFilterProperties(this.collaborationDisciplineKey));
    }
    if (this.currentQueryParams.competition_type) {
      this.filtersForm.get('competition_type').patchValue(this.currentQueryParams.competition_type);
    } else if (!!this.getFilterProperties(this.collaborationTypeKey)) {
      this.filtersForm.get('competition_type').patchValue(this.getFilterProperties(this.collaborationTypeKey));
    }
    if (this.currentQueryParams.competitionYear) {
      this.filtersForm.get('competitionYear').patchValue(this.currentQueryParams.competitionYear);
    } else if (!!this.getFilterProperties(this.collaborationYearKey)) {
      this.filtersForm.get('competitionYear').patchValue(this.getFilterProperties(this.collaborationYearKey));
    } else {
      if (!this.helpers.isInPlayWright) this.filtersForm.get('competitionYear').patchValue('upcoming');
    }

    this.updateQueryParams();
  }

  ngOnInit() {
    this.titleService.setTitle('AC - Collaborations');
    const filtersValueChanges$: Observable<Partial<Organisation> | any> = this.filtersForm.valueChanges.pipe(
      tap(filters => {
        if (filters.country || filters.discipline || filters.competition_type || filters.competitionYear || filters.freeTextFilter?.length > 3) {
          // This is just to log the properties that actually have values
          this.helpers.functionalityLog('collaboration_page', 'Filter Selection', {
            filtersUsed: Object.keys(filters).reduce((objectToLog, propToLog) => {
              if (filters[propToLog]) objectToLog[propToLog] = filters[propToLog];
              return objectToLog;
            }, {}),
          });
        }
      }),
      startWith(this.filtersForm.getRawValue()),
      filter(_ => !doesFormHaveErrors(this.filtersForm)),
      map(_ => this.filtersForm.getRawValue()),
      tap((filtersFormValue: Partial<Organisation> | any) =>
        this.setFiltersInLocalStorage(
          filtersFormValue.country,
          filtersFormValue.discipline,
          filtersFormValue.competition_type,
          filtersFormValue.competitionYear,
        ),
      ),
    );

    this.competitions$ = combineLatest([
      filtersValueChanges$,
      this.organisationProjectionService.collaborationCompetitionProjections$,
      this.shouldLoadAllCompetitions$,
    ]).pipe(
      tap(() => this.updateQueryParams()),
      tap(([_, projection, __]) => {
        if (projection) {
          this.loadingOverlayService.disable('load-collaborations');
        }
      }),
      filter(([_, projections, __]: [any, CollaborationCompetitionProjection[], boolean]) => !!projections),
      map(([_, projections, __]: [any, CollaborationCompetitionProjection[], boolean]): [any, CollaborationCompetitionProjection[], boolean] => {
        const projectionsThatUsersCanSee: CollaborationCompetitionProjection[] = this.filterCompetitionsThatUsersAreNotAllowedToSee(projections);
        return [_, projectionsThatUsersCanSee, __];
      }),
      map(([_, projections, __]: [any, CollaborationCompetitionProjection[], boolean]) => {
        // Here we add more info to the projections, so we don't need to do extra computations in the template
        const projectionsWithMoreProps: CollaborationCompetitionProjection[] = this.addPropertiesToProjectionsForTemplateUse(projections);
        return [_, projectionsWithMoreProps, __];
      }),
      tap(([_, projections, __]: [any, CollaborationCompetitionProjection[], boolean]) => {
        this.initializeCompetitionYearsArray(projections);
      }),
      map(([filters, projections, __]: [any, CollaborationCompetitionProjection[], boolean]) =>
        this.filterProjections(
          filters.country,
          filters.discipline,
          filters.competition_type,
          filters.competitionYear,
          filters.freeTextFilter,
          Object.values(projections),
          __,
        ),
      ),
      tap(([projections, shouldForceLoadAllCompetitions]: [CollaborationCompetitionProjection[], boolean]) => {
        // Set the real amount of projections that were fetched and filtered by the selection and input filters
        this.amountOfResultsFetched = projections.length;
        this.setIfThereAreModeThanThirtyCompetitions(projections, shouldForceLoadAllCompetitions);
      }),
      map(([projections, shouldForceLoadAllCompetitions]: [CollaborationCompetitionProjection[], boolean]): CollaborationCompetitionProjection[] =>
        shouldForceLoadAllCompetitions ? projections : projections.filter((_, index) => index < 30),
      ),
    );

    // We need this because if we deeplink to this page it wouldn't log with the correct user email
    this.userService.userEmail$.pipe(untilDestroyed(this)).subscribe(_ => {
      this.helpers.functionalityLog('collaboration_page', 'Entering collaborations');
    });
  }

  filterCompetitionsThatUsersAreNotAllowedToSee(projections: CollaborationCompetitionProjection[]): CollaborationCompetitionProjection[] {
    return Object.values(projections).filter((projection: CollaborationCompetitionProjection) => !projection.isHiddenOrganisation);
  }

  addPropertiesToProjectionsForTemplateUse(projections: CollaborationCompetitionProjection[]): CollaborationCompetitionProjection[] {
    return projections.map((projection: CollaborationCompetitionProjection) => {
      const start_date: string = this.getDateStringFromSeconds(projection.startDateSecondsAsNumber);
      const end_date: string = this.getDateStringFromSeconds(projection.endDateSecondsAsNumber);

      projection.panelStateOpen = false;
      projection.endDateString = end_date;
      projection.startDateString = start_date;

      return projection;
    });
  }

  initializeCompetitionYearsArray(projections: CollaborationCompetitionProjection[]) {
    const competitionYearsSet: Set<string> = new Set<string>();
    Object.values(projections).forEach((projection: CollaborationCompetitionProjection) => {
      projection.startDateSecondsAsNumber ? competitionYearsSet.add(DateUtils.getYearFromSeconds(projection.startDateSecondsAsNumber)) : null;
    });
    this.competitionYears = Array.from(competitionYearsSet).sort((a: string, b: string) => +b - +a);
  }

  setIfThereAreModeThanThirtyCompetitions(projections: CollaborationCompetitionProjection[], shouldForceLoadAllCompetitions: boolean) {
    if (!shouldForceLoadAllCompetitions) {
      this.areThereMoreThanThirtyCompetitions = projections.length > 30;
    } else {
      this.areThereMoreThanThirtyCompetitions = false;
    }
  }

  setFiltersInLocalStorage(country: string, discipline: string, competitionType: string, competitionYear: string) {
    this.collaborationsInformationService.setFilterProperties(this.collaborationCountryKey, country);
    this.collaborationsInformationService.setFilterProperties(this.collaborationDisciplineKey, discipline);
    this.collaborationsInformationService.setFilterProperties(this.collaborationTypeKey, competitionType);
    this.collaborationsInformationService.setFilterProperties(this.collaborationYearKey, competitionYear);
  }

  updateQueryParams() {
    let queryParams: Params = {
      country: this.filtersForm.get('country')?.value,
      discipline: this.filtersForm.get('discipline')?.value,
      competition_type: this.filtersForm.get('competition_type')?.value,
      competitionYear: this.filtersForm.get('competitionYear')?.value,
    };

    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: queryParams,
      queryParamsHandling: 'merge', // remove to replace all query params by provided
    });
  }

  filterProjections(
    country: string,
    discipline: DisciplineType,
    competitionType: CompetitionType,
    competitionYear: string,
    freeTextFilter: string,
    competitionsProjections: CollaborationCompetitionProjection[],
    shouldForceLoadAllCompetitions: boolean,
  ): [CollaborationCompetitionProjection[], boolean] {
    let allCompetitionsProjections: CollaborationCompetitionProjection[] = competitionsProjections;
    if (freeTextFilter) {
      const fuseOptions = {
        keys: ['name', 'country'],
        threshold: 0.2,
      };

      let fuse = new Fuse(competitionsProjections, fuseOptions);
      allCompetitionsProjections = fuse.search(freeTextFilter).map(fuseReturnObject => fuseReturnObject.item);
    }

    return [
      allCompetitionsProjections
        .filter((projection: CollaborationCompetitionProjection) => {
          let shouldFilterByYear: boolean;
          if (competitionYear) {
            if (competitionYear === 'upcoming') {
              const start_date: Date = DateUtils.convertSecondsFromMidnightToDate(projection.startDateSecondsAsNumber);
              const today: Date = new Date();
              shouldFilterByYear = DateUtils.isFirstDateBeforeSecond(today, start_date);
            } else shouldFilterByYear = DateUtils.getYearFromSeconds(projection.startDateSecondsAsNumber) === competitionYear;
          } else shouldFilterByYear = true;

          return (
            (country ? projection.country === country : true) &&
            (discipline ? projection.discipline === discipline : true) &&
            (competitionType ? projection.competition_type === competitionType : true) &&
            shouldFilterByYear
          );
        })
        .sort((projectionA: CollaborationCompetitionProjection, projectionB: CollaborationCompetitionProjection): number => {
          if (competitionYear && competitionYear === 'upcoming') return this.sortCompetitionProjectionsByDateAndName(projectionA, projectionB, false);
          else return this.sortCompetitionProjectionsByDateAndName(projectionA, projectionB, true);
        }),
      shouldForceLoadAllCompetitions,
    ];
  }

  createForm() {
    this.filtersForm = this.fb.group({
      country: ['', {}],
      competition_type: ['', {}],
      discipline: ['', {}],
      competitionYear: ['', {}],
      freeTextFilter: ['', {}],
    });
  }

  getCompImage(competition: CollaborationCompetitionProjection): string {
    if (!!competition.competitionPosterUrl) return competition.competitionPosterUrl;
    else if (!!competition.logoUrl) return competition.logoUrl;
    else if (!!competition.associatedOrganisationLogoUrl) return competition.associatedOrganisationLogoUrl;
    else return '/assets/logos/minimized_logo_blue.svg';
  }

  getFilterProperties(key: string): any {
    return this.localStorage.getItem(key);
  }

  navigateToLiveCompetition(competitionKey: string) {
    // Query Params added, so we know that we navigated from the collaboration page
    this.helpers.functionalityLog('collaboration_page', 'Competition Clicked', { competitionKey });
    // skipLocationChange => used to be able to navigate from live competitions to collaborations without being necessary to navigate back two times
    this.router.navigate([`competition/${competitionKey}/information`], { skipLocationChange: true });
  }

  sortCompetitionProjectionsByDateAndName(
    projectionA: CollaborationCompetitionProjection,
    projectionB: CollaborationCompetitionProjection,
    shouldSortInDescendingOrder: boolean,
  ): number {
    const dateInSecondsA: number = projectionA.startDateSecondsAsNumber;
    const dateInSecondsB: number = projectionB.startDateSecondsAsNumber;

    if (dateInSecondsA !== dateInSecondsB) return shouldSortInDescendingOrder ? dateInSecondsB - dateInSecondsA : dateInSecondsA - dateInSecondsB;
    else {
      const nameA: string = projectionA.name.toLowerCase();
      const nameB: string = projectionB.name.toLowerCase();

      if (nameA < nameB) {
        return -1;
      }
      if (nameA > nameB) {
        return 1;
      }
      return 0;
    }
  }

  togglePanelState(competition: CollaborationCompetitionProjection) {
    competition.panelStateOpen = !competition.panelStateOpen;
  }

  getDateStringFromSeconds(seconds: number): string {
    return seconds ? DateUtils.convertSecondsFromMidnightToDateString(seconds) : '';
  }

  getMonthAndYearForDisplay(seconds: number): string {
    if (seconds === null || seconds === undefined) return;
    const Date: Date = DateUtils.convertSecondsFromMidnightToDate(seconds);
    return DateUtils.getMonthAndYearString(Date);
  }

  isNewMonth(previousCompetitionDateInSeconds: number, currentCompetitionDateInSeconds: number, isFirstIteration: boolean): boolean {
    if (isFirstIteration) return true;
    else {
      const monthOfPreviousCompetition: number = getMonth(DateUtils.convertSecondsFromMidnightToDate(previousCompetitionDateInSeconds));
      const monthOfCurrentCompetition: number = getMonth(DateUtils.convertSecondsFromMidnightToDate(currentCompetitionDateInSeconds));
      return monthOfPreviousCompetition !== monthOfCurrentCompetition;
    }
  }

  async navigateToRegistration(event, organisation: CollaborationCompetitionProjection) {
    // stops the levels div from opening when clicking the registration button
    event.stopPropagation();
    if (organisation.is_registration_enabled) {
      this.router.navigate([`registrations/configuration/${organisation.key}`], { replaceUrl: true });
    } else if (organisation.has_registrations) {
      if (organisation.is_open_for_registrations) {
        const clubsWhereUserIsManagerOf: Organisation[] = await firstValueFrom(this.organisationService.connectToClubsList$());
        if (clubsWhereUserIsManagerOf.length === 0) {
          // If we change the toast string we have to change the string in the collaboration page testsuite as well
          this.toastService.show({
            text: 'You are not a manager of any club, please contact your club so they can add you as a manager to be able to register to this competition',
            type: 'warning',
          });
          return;
        }
        let clubToCreateRegistration: Organisation;
        if (clubsWhereUserIsManagerOf.length > 1) {
          let clubOptionsToSelect: DropDownDialogOption[] = [];
          clubsWhereUserIsManagerOf.forEach(club => {
            clubOptionsToSelect.push({
              value: club.$key,
              label: club.name,
            });
          });
          let config: MatDialogConfig<DropDownDialogData> = {
            data: {
              title: 'Select the club to start registration',
              options: clubOptionsToSelect,
              selectionRequired: true,
              hasCancelButton: true,
              placeholder: 'Select a Club...',
            },
            autoFocus: false,
          } as MatDialogConfig;

          let dialogRef = this.dialog.open(DropdownDialogComponent, config);
          let result: any = await lastValueFrom(dialogRef.afterClosed());

          if (result?.clicked === 'ok') {
            clubToCreateRegistration = clubsWhereUserIsManagerOf.find(club => club.$key === result.value);
          } else {
            return;
          }
        } else {
          clubToCreateRegistration = clubsWhereUserIsManagerOf[0];
        }

        // we need this because sometimes the currentOrganisation might be null which would cause the navigation below to fail.
        // An example is whenever you were on org admin, and you went to live competitions and then to collaborations and tried to register the navigation would fail
        this.currentOrganisationService.setOrganisation(clubToCreateRegistration.$key);
        await this.loadingOverlayService.enable('navigateToClubRegistrations', 'Navigation to club registrations...');
        await firstValueFrom(this.currentOrganisationService.organisation$.pipe(filter(org => org?.$key === clubToCreateRegistration.$key)));
        await this.router.navigate([`/organisation-admin/${clubToCreateRegistration.$key}/members`], {
          replaceUrl: true,
          queryParams: { clubtab: 'Registrations', organisationKey: organisation.key, organisationDiscipline: organisation.discipline },
        });
        await this.loadingOverlayService.disable('navigateToClubRegistrations');
      } else {
        // If we change the toast string we have to change the string in the collaboration page testsuite as well
        this.toastService.show({ text: 'This competition is not accepting registrations right now', type: 'warning' });
      }
    }
  }

  isRegistrationButtonDisabled(organisation: CollaborationCompetitionProjection): boolean {
    return !!this.getRegistrationTootipButtonText(organisation);
  }

  getRegistrationTootipButtonText(organisation: CollaborationCompetitionProjection): string {
    const todaySeconds: number = Timestamp.now().seconds;
    if (organisation.endDateSecondsAsNumber < todaySeconds) return 'This competition has already ended';
    else if (!this.authenticationService.isUserLoggedIn()) return 'Please sign in to register';
    else return null;
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions