Skip to content

Commit

Permalink
v3.27.0 (#214)
Browse files Browse the repository at this point in the history
* Certificates revamp MVP

* Light revamp of name approvals
  • Loading branch information
sei-bstein authored Dec 20, 2024
1 parent a717b12 commit a5b990b
Show file tree
Hide file tree
Showing 45 changed files with 670 additions and 324 deletions.
5 changes: 5 additions & 0 deletions projects/gameboard-ui/src/app/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ import { ToSupportCodePipe } from '@/standalone/core/pipes/to-support-code.pipe'
import { IfHasPermissionDirective } from '@/standalone/directives/if-has-permission.directive';
import { FeedbackTemplatePickerComponent } from "../feedback/components/feedback-template-picker/feedback-template-picker.component";
import { UserPickerComponent } from '@/standalone/users/user-picker/user-picker.component';
import { CertificateTemplatePickerComponent } from '@/certificates/components/certificate-template-picker/certificate-template-picker.component';
import { CertificatePreviewerComponent } from '@/certificates/components/certificate-previewer/certificate-previewer.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -160,6 +162,7 @@ import { UserPickerComponent } from '@/standalone/users/user-picker/user-picker.
RouterModule.forChild([
{
path: '', component: AdminPageComponent, title: "Admin", children: [
{ path: "certificates/templates/:templateId/preview", component: CertificatePreviewerComponent, title: "Certificate Template Preview" },
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
{ path: 'dashboard', component: DashboardComponent },
{
Expand Down Expand Up @@ -225,6 +228,8 @@ import { UserPickerComponent } from '@/standalone/users/user-picker/user-picker.
SafeUrlPipe,
SpinnerComponent,
ToSupportCodePipe,
CertificatePreviewerComponent,
CertificateTemplatePickerComponent,
FeedbackTemplatePickerComponent,
UserPickerComponent,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
</div>

<div class="col-12 mt-5">
<h4>Player Feedback</h4>
<app-feedback-template-picker labelText="Game Feedback Template"
[(templateId)]="game.feedbackTemplateId"
(select)="handleGameFeedbackTemplateChanged($event)"></app-feedback-template-picker>
Expand All @@ -157,46 +158,14 @@
</div>

<div class="col-12 mt-5">
<div class="form-group pb-0 pt-1">
<label for="certificateTemplate-input">Certificate Template</label>
<textarea rows="11" type="text" class="form-control" id="certificateTemplate-input"
name="certificateTemplate" [(ngModel)]="game.certificateTemplate"></textarea>
<small>design with HTML and inline/internal CSS; use a 11:8.5 aspect ratio</small>
<button class="btn btn-sm btn-link-white" [(ngModel)]="showCertificateInfo"
[ngModelOptions]="{standalone: true}" btnCheckbox>
<fa-icon [icon]="fa.infoCircle"></fa-icon>
</button>
<div class="ml-4 px-2" *ngIf="showCertificateInfo">
<p class="cert-info mb-2">
Insert dynamic content by referring to a property with double-curly syntax
<code>{{"\{\{game_name\}\}"}}</code>. For example,
<code>&lt;h1&gt;{{"\{\{leaderboard_name\}\}"}}&lt;/h1&gt;</code>. <br>
The following properties will get replaced when a player certificate renders:
</p>
<pre class="mb-1">
game_name &mdash; Name of this game
competition &mdash; Competition type of this game
season &mdash; Season of this game
round &mdash; Round of this game
track &mdash; Track of this game
user_name &mdash; Individual user's approved name
score &mdash; Total player score for this game
rank &mdash; Final leaderboard ranking of the player
leaderboard_name &mdash; Approved name for either team or individual
date &mdash; Date player's session ended for this game
player_count &mdash; Number of players who participated in this game
team_count &mdash; Number of teams who participated in this game</pre>
<p class="cert-info mb-2">
Tip: Create an outer div with fixed height and width and <code>position: relative</code>.
Create inner
divs with <code>position: absolute; text-align: center;</code> and set textbox width and X/Y
position with
<code>top: __px; left: __px; width: __px;</code>.
To add a background image, use
<code>background-size: 100% 100%; background-image: url('URL_HERE');</code>
</p>
</div>
</div>
<h4>Completion Certificates</h4>
<app-certificate-template-picker labelText="Competitive Certificate Template"
[selectedTemplateId]="game.certificateTemplateId"
(selected)="handleCertificateTemplateChanged($event)"></app-certificate-template-picker>
<app-certificate-template-picker labelText="Practice Certificate Template"
defaultOptionText="[use global practice area template]"
[selectedTemplateId]="game.practiceCertificateTemplateId"
(selected)="handlePracticeCertificateTemplateChanged($event)"></app-certificate-template-picker>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UnsubscriberService } from '@/services/unsubscriber.service';
import { ToastService } from '@/utility/services/toast.service';
import { ActivatedRoute } from '@angular/router';
import { FeedbackTemplateView } from '@/feedback/feedback.models';
import { CertificateTemplateView } from '@/certificates/certificates.models';

export type SelectedSubTab = "settings" | "modes" | "registration";

Expand Down Expand Up @@ -86,6 +87,24 @@ export class GameCenterSettingsComponent implements AfterViewInit {
}
}

protected async handleCertificateTemplateChanged(template?: CertificateTemplateView) {
if (!this.game) {
throw new Error("Game is required");
}

this.game.certificateTemplateId = template?.id;
await firstValueFrom(this.gameService.update(this.game));
}

protected async handlePracticeCertificateTemplateChanged(template?: CertificateTemplateView) {
if (!this.game) {
throw new Error("Game is required");
}

this.game.practiceCertificateTemplateId = template?.id;
await firstValueFrom(this.gameService.update(this.game));
}

protected async handleChallengesFeedbackTemplateChanged(template?: FeedbackTemplateView) {
if (!this.game) {
throw new Error("Game is required");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,49 @@ <h6>Score</h6>
</div>

<ng-container>
<h4 class="mt-4 px-3">Team Management</h4>
<div class="name-management">
<ul class="container mx-0">
<li class="row">
<div class="col-3">
<label class="mb-0 text-muted">Current Name</label>
</div>
<div class="col-4">
<label class="mb-0 text-muted">Requested Name</label>
</div>
<div class="col-3">
<label class="mb-0 text-muted" for="reason-input">Reason for Change</label>
</div>
</li>
<li *ngFor="let player of team.players" class="row mb-2">
<div class="col-3">
<div class="fs-10 player-current-name">{{ player.name }}</div>
</div>
<div class="col-4">
<input type="text" class="form-control" name="pending-name-input-{{player.id}}" type="text"
[placeholder]="(player.pendingName ? 'Name this player (requested: ' + player.pendingName + ')' : 'No pending name change requests from this player')"
minlength="2" [value]="player.pendingName" #pendingNameInput>
</div>
<div class="col-3">
<select name="reason-select" class="form-control" #reasonSelect>
<option [value]="''">[not disapproved]</option>
<option *ngFor="let reason of reasons" [value]="reason">
{{reason}}
</option>
</select>
</div>
<div>
<button type="button" class="btn btn-warning mr-2"
[disabled]="pendingNameInput.value.length < 2"
(click)="approveName(player.id, { name: pendingNameInput.value, revisionReason: reasonSelect.value })">Approve</button>
<div class="mt-4 px-3">
<h4>Team Management</h4>
<table class="width-100">
<col class="width-20">
<col class="width-20">
<col class="width-20">
<col class="width-20">
<col class="width-20">

<button type="button" class="btn btn-danger"
[disabled]="team.players.length == 1 || (player.id === team.captain.id)"
(click)="handleRemovePlayerConfirm(player)">Remove</button>
</div>
</li>
</ul>
<thead>
<th>Name</th>
<th>Requested Name</th>
<th>Override Name</th>
<th>Status</th>
<th></th>
</thead>

<tbody>
<tr *ngFor="let player of team.players">
<td>{{ player.name }}</td>
<td>{{ player.pendingName || "--" }}</td>
<td>
<input type="text" class="form-control" placeholder="Enter a name" #overrideName>
</td>
<td>
<select name="reason-select" class="form-control" #reasonSelect>
<option [value]="''">[approved]</option>
<option *ngFor="let reason of reasons" [value]="reason">
{{reason}}
</option>
</select>
</td>
<td class="d-flex align-items-center justify-content-center">
<button type="button" class="btn btn-warning mr-2"
(click)="updateNameChangeRequest(player.id, overrideName.value, { requestedName: player.pendingName || '', approvedName: player.name || '', status: reasonSelect.value })">Update</button>

<button type="button" class="btn btn-danger"
[disabled]="team.players.length == 1 || (player.id === team.captain.id)"
(click)="handleRemovePlayerConfirm(player)">Remove</button>
</td>
</tr>
</tbody>
</table>
</div>
</ng-container>

Expand All @@ -81,9 +82,6 @@ <h4 class="mt-4 px-3">Session</h4>
</div>

<h4 class="px-3 mr-1">Timeline</h4>
<!-- <div class="d-flex align-items-baseline">
</div> -->
<div class="timeline-container px-3 mb-4">
<app-team-event-horizon [teamId]="team.id"></app-team-event-horizon>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AdminService } from '@/api/admin.service';
import { ToastService } from '@/utility/services/toast.service';
import { TeamService } from '@/api/team.service';
import { SimpleEntity } from '@/api/models';
import { UpdatePlayerNameChangeRequest } from '@/api/admin.models';

@Component({
selector: 'app-game-center-team-detail',
Expand Down Expand Up @@ -106,20 +107,29 @@ export class GameCenterTeamDetailComponent implements OnInit {
this.showChallengeYaml = isExpanding;
}

protected async approveName(playerId: string, args: { name: string, revisionReason: string }) {
const finalName = args.name.trim();
await this.adminService.approvePlayerName(playerId, args);
protected async updateNameChangeRequest(playerId: string, overrideName: string, args: UpdatePlayerNameChangeRequest) {
if (!args.status) {
args.approvedName = overrideName || args.requestedName;
}

// tell the API
await this.adminService.updatePlayerNameChangeRequest(playerId, args);

// rebind
const player = this.team.players.find(p => p.id === playerId);
if (player) {
player.pendingName = "";
player.name = args.name;
player.pendingName = player.pendingName == args.approvedName ? "" : player.pendingName;
player.name = args.approvedName;
}

if (this.team.captain.id === playerId)
this.team.name = args.name;
this.team.name = args.approvedName;

this.toastService.showMessage(`This player's name has been changed to **${args.approvedName}**.${args.status ? ` (reason: **${args.status}**)` : ""}`);
}

this.toastService.showMessage(`This player's name has been changed to **${args.name}**.${args.revisionReason ? ` (reason: **${args.revisionReason}**)` : ""}`);
protected handleAdminNameChangeRequest() {
this.modalService.openConfirm({ bodyContent: "Are you sure?" });
}

protected async handleRemovePlayerConfirm(player: SimpleEntity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,13 @@ export class PlayerNamesComponent {
}

async approveName(model: Player) {
const requested = model.name;
const approved = model.approvedName;

model.approvedName = model.name;
model.nameStatus = "";
model.pendingName = "";
await this.adminService.approvePlayerName(model.id, { name: model.name });
await this.adminService.updatePlayerNameChangeRequest(model.id, { approvedName: approved, requestedName: requested, status: "" });
}

resetName(model: Player): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,13 @@ <h2>Suggested searches</h2>
<div class="greeting-container settings-section">
<h2>Certificate Template</h2>
<p>
Players are able to save certificates in PDF format for each practice challenge they fully complete. Use
this field to customize the appearance of these certificates.
Players are able to save certificates in PDF format for each practice challenge they fully complete. Choose
a template below to customize the appearance of these certificates.
</p>

<textarea class="form-control" (input)="handleSettingsChanged(ctx.settings)"
[placeholder]="certificateHtmlPlaceholder" [(ngModel)]="ctx.settings.certificateHtmlTemplate"
rows="5"></textarea>
<button type="button" class="btn btn-link text-success" (click)="handleShowCertificateTemplateHelp()">
How do I create a certificate template?
</button>
<app-certificate-template-picker [selectedTemplateId]="ctx.settings.certificateTemplateId" [hideLabel]="true"
(selected)="handleCertificateTemplateSelect($event)"
defaultOptionText="[no global practice certificate template]"></app-certificate-template-picker>
</div>

<div class="concurrent-sessions-container settings-section">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { CertificateTemplateView } from '@/certificates/certificates.models';
import { PracticeModeSettings } from '@/prac/practice.models';
import { MarkdownHelpersService } from '@/services/markdown-helpers.service';
import { ModalConfirmService } from '@/services/modal-confirm.service';
import { PracticeService } from '@/services/practice.service';
import { UnsubscriberService } from '@/services/unsubscriber.service';
import { Component, OnInit } from '@angular/core';
import { Subject, debounceTime, firstValueFrom, map, of, tap } from 'rxjs';
import { Subject, debounceTime, firstValueFrom, map } from 'rxjs';

interface PracticeSettingsContext {
introTextPlaceholder: string;
Expand All @@ -23,18 +24,6 @@ export class PracticeSettingsComponent implements OnInit {
private _startUpdate$ = new Subject<PracticeModeSettings>();
protected suggestedSearchesLineDelimited = "";

protected certificateHtmlPlaceholder = [
"Enter an HTML template here which will be used to create certificates for the Practice Area. For each challenge they fully solve, players will be able to print a PDF of this certificate.",
"You can use several variables to display information about the player's performance on the challenge by including these \"magic strings\" in your template.They include: ",
`- {{playerName}} - The player's approved username
- {{score}} - The player's score on the challenge
- {{date}} - The date the player completed the challenge
- {{challengeName}} - The name of the challenge
- {{season}} - The season the challenge was originally played in competitive mode
- {{track}} - The track upon which the challenge was originally placed in competitive mode
- {{time}} - The amount of time the player spent solving the challenge`
].join("\n\n");

constructor(
private modalService: ModalConfirmService,
private markdownHelpers: MarkdownHelpersService,
Expand All @@ -55,13 +44,13 @@ export class PracticeSettingsComponent implements OnInit {
this.suggestedSearchesLineDelimited = this.ctx.settings.suggestedSearches.join("\n");
}

protected handleShowCertificateTemplateHelp() {
this.modalService.open({
title: "Creating a certificate template",
bodyContent: this.certificateHtmlPlaceholder,
modalClasses: ["modal-lg"],
renderBodyAsMarkdown: true
});
protected handleCertificateTemplateSelect(template?: CertificateTemplateView) {
if (!this.ctx) {
return;
}

this.ctx.settings.certificateTemplateId = template?.id;
this._startUpdate$.next(this.ctx.settings);
}

protected handleShowSuggestedSearchesNote() {
Expand Down
11 changes: 6 additions & 5 deletions projects/gameboard-ui/src/app/api/admin.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ export interface AppActiveChallengeGame {
isTeamGame: boolean;
}

export interface ApprovePlayerNameRequest {
name: string;
revisionReason?: string;
}

export interface GetAppActiveChallengesResponse {
specs: AppActiveChallengeSpec[];
}
Expand Down Expand Up @@ -89,3 +84,9 @@ export interface SendAnnouncement {
title?: string;
teamId?: string;
}

export interface UpdatePlayerNameChangeRequest {
approvedName: string;
requestedName: string;
status: string;
}
4 changes: 2 additions & 2 deletions projects/gameboard-ui/src/app/api/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { DateTime } from 'luxon';
import { Observable, firstValueFrom, map, tap } from 'rxjs';
import { ApiUrlService } from '@/services/api-url.service';
import { ApprovePlayerNameRequest, GetAppActiveChallengesResponse, GetAppActiveTeamsResponse, GetPlayersCsvExportResponse, GetPlayersCsvExportResponsePlayer, GetSiteOverviewStatsResponse, SendAnnouncement } from './admin.models';
import { UpdatePlayerNameChangeRequest, GetAppActiveChallengesResponse, GetAppActiveTeamsResponse, GetPlayersCsvExportResponse, GetPlayersCsvExportResponsePlayer, GetSiteOverviewStatsResponse, SendAnnouncement } from './admin.models';
import { PlayerMode } from './player-models';
import { GameCenterContext, GameCenterPracticeContext, GameCenterTeamsRequestArgs, GameCenterTeamsResults, GetGameCenterPracticeContextRequest } from '@/admin/components/game-center/game-center.models';

Expand All @@ -13,7 +13,7 @@ export class AdminService {
private apiUrl: ApiUrlService,
private http: HttpClient) { }

async approvePlayerName(playerId: string, request: ApprovePlayerNameRequest) {
async updatePlayerNameChangeRequest(playerId: string, request: UpdatePlayerNameChangeRequest) {
return await firstValueFrom(this.http.put(this.apiUrl.build(`admin/players/${playerId}/name`), request));
}

Expand Down
Loading

0 comments on commit a5b990b

Please sign in to comment.