Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.17.0 #174

Merged
merged 19 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,25 +1,69 @@
<div class="modal-content" *ngIf="game">
<div class="modal-header">
<h2 class="modal-title">Add {{game.isTeamGame ? "Team" : "Player"}}</h2>
<h2 class="modal-title">Add {{isTeamGame ? "Team" : "Player"}}</h2>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</div>

<div class="modal-body" *ngIf="!isWorking; else loading">
<div class="modal-body my-2" *ngIf="!isWorking; else loading">
<app-error-div [errors]="errors"></app-error-div>
<div class="form-group">
<input type="text" class="form-control" [(ngModel)]="addUserId"
placeholder="This is totally the final interface, just so you know.">
<div class="input-group">
<input [(ngModel)]="searchTerm" appAutofocus [typeahead]="typeaheadSearch$" [typeaheadAsync]="true"
typeaheadOptionField="name" class="form-control" (typeaheadOnSelect)="handleTypeaheadSelect($event)"
[typeaheadItemTemplate]="searchResultsTemplate" placeholder="Search by name or user ID">
<div class="input-group-append">
<span class="input-group-text"><fa-icon [icon]="fa.search"></fa-icon></span>
</div>
</div>

<div class="selection-container">
<ul class="selection-list d-flex flex-wrap">
<li *ngFor="let user of selectedUsers" class="d-flex align-items-center mr-3 py-4 user-list-item">
<app-avatar class="mr-2" [imageUrl]="user.sponsor | sponsorToLogoUri" [tooltip]="user.sponsor.name"
size="tiny"></app-avatar>
<div class="user-info">
<div class="fs-11">{{ user.name }}</div>
<div class="fs-08 link-button info-text" [appCopyOnClick]="user.id" tooltip="Copy this user ID">
{{ user.id | slice:0:8 }}
</div>
</div>
</li>
</ul>
</div>
</div>

<div class="modal-footer">
<!-- <div *ngIf="this.selectedUsers.length >= 2 && isTeamGame" class="form-group form-check flex-grow-1">
<input type="checkbox" class="form-check-input" id="addAsTeam">
<label class="form-check-label" for="addAsTeam">Add these users as a team</label>
</div> -->

<button type="button" class="btn link-button" (click)="close()">Cancel</button>
<button type="button" class="btn btn-success" (click)="handleAddClick(addUserId)">Add this player</button>
<button type="button" class="btn btn-success" (click)="handleAddClick()"
[disabled]="selectedUsers.length === 0 || (game.minTeamSize && (selectedUsers.length < game.minTeamSize)) || (game.maxTeamSize && (selectedUsers.length > game.maxTeamSize))">
Add {{ selectedUsers.length >= 2 ? "These " + selectedUsers.length + " Users" : "This User" }}
</button>
</div>
</div>

<ng-template #loading>
<app-spinner></app-spinner>
</ng-template>

<ng-template #noResults class="text-center">
<div class="text-center my-3">
<em class="gray-text">Use the box above to search for the user(s) you want to enroll in this game.</em>
</div>
</ng-template>

<ng-template #searchResultsTemplate let-user="item">
<div class="d-flex align-items-center">
<app-avatar class="mr-3" [imageUrl]="user.sponsor | sponsorToLogoUri" [tooltip]="user.sponsor.name"
size="tiny"></app-avatar>
<div class="user-info">
<div>{{ user.name }}</div>
<em class="gray-text fs-08">{{ user.id | slice:0:8}}</em>
</div>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,48 +1,95 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Observable, Observer, Subject, debounceTime, filter, firstValueFrom, switchMap, tap } from 'rxjs';
import { fa } from "@/services/font-awesome.service";
import { TeamService } from '@/api/team.service';
import { ApiUser } from '@/api/user-models';
import { UserService } from '@/api/user.service';
import { ModalConfirmService } from '@/services/modal-confirm.service';
import { Component, OnInit } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { TypeaheadMatch } from 'ngx-bootstrap/typeahead';
import { AdminEnrollTeamResponse } from '@/api/teams.models';

@Component({
selector: 'app-admin-enroll-team-modal',
templateUrl: './admin-enroll-team-modal.component.html',
styleUrls: ['./admin-enroll-team-modal.component.scss']
})
export class AdminEnrollTeamModalComponent implements OnInit {
@ViewChild("searchBox") searchBoxRef?: ElementRef<HTMLInputElement>;

game?: {
id: string;
name: string;
isTeamGame: boolean;
minTeamSize: number;
maxTeamSize?: number;
};
onConfirm?: (createdTeam: AdminEnrollTeamResponse) => Promise<void> | void;

private _search$ = new Subject<string>();

protected addUserId = "";
protected canAddAsTeam = false;
protected errors: any[] = [];
protected fa = fa;
protected isTeamGame = false;
protected isWorking = false;
protected searchTerm = "";
protected selectedUsers: ApiUser[] = [];
protected typeaheadSearch$ = new Observable((observer: Observer<string | undefined>) => observer.next(this.searchTerm)).pipe(
filter(s => s?.length >= 3),
switchMap(search => this.userService.list({
eligibleForGameId: this.game!.id,
excludeIds: [...this.selectedUsers.map(u => u.id)],
term: search,
})),
);

constructor(
private modalConfirmService: ModalConfirmService,
private teamService: TeamService) { }
private teamService: TeamService,
private userService: UserService) { }

ngOnInit(): void {
if (!this.game)
throw new Error("Can't resolve the game.");

this.isTeamGame = !this.game.maxTeamSize || this.game.maxTeamSize >= 2;
}

protected close() {
this.modalConfirmService.hide();
}

protected async handleAddClick(userId: string) {
protected async handleAddClick() {
this.errors = [];
if (!this.game?.id)
return;

if (!this.selectedUsers.length)
return;

try {
await firstValueFrom(this.teamService.adminEnroll({ userIds: [this.addUserId], gameId: this.game.id }));
const userIds = this.selectedUsers.map(u => u.id);
const result = await firstValueFrom(this.teamService.adminEnroll({ userIds, gameId: this.game.id }));

if (this.onConfirm)
await this.onConfirm(result);

this.modalConfirmService.hide();
}
catch (err: any) {
this.errors.push(err);
}
}

protected handleTypeaheadSelect(apiUserResult: TypeaheadMatch) {
if (!this.selectedUsers.some(u => u.id === apiUserResult.item.id)) {
this.selectedUsers.push(apiUserResult.item);
this.selectedUsers.sort((a, b) => a.approvedName.localeCompare(b.approvedName));
}

this.searchTerm = "";
}

protected search(input: string) {
this._search$.next(input.length >= 3 ? input : "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,10 @@ export class PlayerRegistrarComponent {
});
}

unenroll(model: Player): void {
async unenroll(model: Player): Promise<void> {
this.isLoading = true;
this.api.unenroll(model.id, true).pipe(first()).subscribe(_ => {
this.refresh$.next(true);
});
await firstValueFrom(this.teamService.unenroll({ teamId: model.teamId }));
this.refresh$.next(true);
}

update(model: Player): void {
Expand Down Expand Up @@ -293,7 +292,12 @@ export class PlayerRegistrarComponent {
content: AdminEnrollTeamModalComponent,
context: {
game: game,
onConfirm: result => {
this.toastService.showMessage(`Enrolled ${result.name}`);
this.refresh$.next(true);
}
},
modalClasses: ["modal-xl"]
});
}

Expand All @@ -304,7 +308,7 @@ export class PlayerRegistrarComponent {
protected confirmReset(request: TeamAdminContextMenuSessionResetRequest) {
this.modalConfirmService.openConfirm({
bodyContent: `
Are you sure you want to reset the session for ${request.player.approvedName}${this.game.allowTeam ? " (and their team)" : ""}?
Are you sure you want to reset the session for ${this.game.allowTeam ? " team" : ""} ${request.player.approvedName}?
${(!request.unenrollTeam ? "" : `
They'll also be unenrolled from the game.`)}
`,
Expand All @@ -317,7 +321,7 @@ export class PlayerRegistrarComponent {

protected confirmUnenroll(player: Player) {
this.modalConfirmService.openConfirm({
bodyContent: `Are you sure you want to unenroll ${player.approvedName}${this.game.allowTeam ? " (and their team)" : ""}?`,
bodyContent: `Are you sure you want to unenroll ${this.game.allowTeam ? " team " : ""}${player.approvedName}?`,
title: `Unenroll ${player.approvedName}?`,
onConfirm: () => {
this.unenroll(player);
Expand Down
21 changes: 14 additions & 7 deletions projects/gameboard-ui/src/app/api/team.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Observable, Subject, map, tap } from "rxjs";
import { SessionEndRequest, SessionExtendRequest, Team } from "./player-models";
import { AdminEnrollTeamRequest, AdminEnrollTeamResponse, AdminExtendTeamSessionResponse, ResetTeamSessionRequest } from "./teams.models";
import { ApiUrlService } from "@/services/api-url.service";
import { ApiDateTimeService } from "@/services/api-date-time.service";
import { unique } from "../../tools";

@Injectable({ providedIn: 'root' })
export class TeamService {
Expand All @@ -18,11 +18,11 @@ export class TeamService {
public teamSessionReset$ = this._teamSessionReset$.asObservable();

constructor(
private apiDates: ApiDateTimeService,
private apiUrl: ApiUrlService,
private http: HttpClient) { }

adminEnroll(request: AdminEnrollTeamRequest): Observable<AdminEnrollTeamResponse> {
request.userIds = unique(request.userIds);
return this.http.post<AdminEnrollTeamResponse>(this.apiUrl.build("admin/team"), request);
}

Expand All @@ -32,6 +32,12 @@ export class TeamService {
);
}

unenroll(request: { teamId: string }) {
return this.http.post(this.apiUrl.build(`team/${request.teamId}/session`), {
unenrollTeam: true
});
}

public get(teamId: string) {
return this.http.get<Team>(this.apiUrl.build(`/team/${teamId}`));
}
Expand Down Expand Up @@ -63,16 +69,17 @@ export class TeamService {
return this.updateSession(model);
}

public resetSession(teamId: string, request: ResetTeamSessionRequest): Observable<void> {
return this.http.post<void>(this.apiUrl.build(`/team/${teamId}/session`), request).pipe(
tap(_ => this._teamSessionReset$.next(teamId))
);
}

private updateSession(request: SessionExtendRequest | SessionEndRequest): Observable<void> {
return this.http.put<any>(this.apiUrl.build("/team/session"), request).pipe(
tap(_ => this._teamSessionsChanged$.next([request.teamId])),
tap(_ => this._playerSessionChanged$.next(request.teamId)),
);
}

public resetSession(teamId: string, request: ResetTeamSessionRequest): Observable<void> {
return this.http.post<void>(this.apiUrl.build(`/team/${teamId}/session`), request).pipe(
tap(_ => this._teamSessionReset$.next(teamId))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>sponsor-avatar works!</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@use "sass:map";
@import "../../../../scss/player-avatars";

.avatar-container {
aspect-ratio: 1 / 1;
background: no-repeat center center;
background-color: #dadada;
background-size: cover;
border: dashed 1px #dadada;
border-radius: 50%;
clip-path: circle(50% at 50% 50%);
display: inline-block;
object-fit: cover;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AvatarSize } from '@/core/models/avatar';
import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { SafeUrl } from '@angular/platform-browser';

@Component({
selector: 'app-avatar',
styleUrls: ['./avatar.component.scss'],
template: `
<div [class]="'avatar-container avatar-size ' + this.sizeClass" [style.background-image]="'url(' + imageUrl + ')'" [tooltip]="tooltip ? tooltip : ''"></div>
`,
})
export class AvatarComponent implements OnChanges {
@Input() imageUrl?: SafeUrl;
@Input() size: AvatarSize = "medium";
@Input() tooltip = "";

@ViewChild("searchBox") searchBox?: ElementRef<Input>;

protected sizeClass = "avatar-size-medium";

ngOnChanges(changes: SimpleChanges): void {
this.sizeClass = `avatar-size-${this.size}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { SafeUrl } from '@angular/platform-browser';
selector: 'app-player-avatar',
template: `
<div [class]="'d-flex position-relative align-items-center justify-content-center player-avatar-component avatar-list-size ' + sizeClass + ' ' + avatarCountClass">
<div [class]="'avatar-container avatar-size ' + this.sizeClass" aria-roledescription="Player avatar icon"
[style.background-image]="avatarUrl" [tooltip]="showSponsorTooltip ? tooltip : ''"></div>
<app-avatar [size]="this.size" [imageUrl]="avatarUrl" [tooltip]="showSponsorTooltip ? tooltip : ''">
</app-avatar>
<app-player-status class="position-absolute status-light" *ngIf="session" [session]="session"></app-player-status>
</div>
</div>
`,
styleUrls: ['./player-avatar.component.scss']
})
Expand Down
Loading
Loading