From 3a71ae221e06837511bb0675ff887f3883bc6070 Mon Sep 17 00:00:00 2001 From: Ben Stein <115497763+sei-bstein@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:43:04 -0500 Subject: [PATCH] v3.24.0-beta0 (#201) * Fix tracking of active prac challenge * Minor cleanup and improved console logging * Bug fixes and challenge report tags filter. * Fix event horizon, game center teams, enrollment report stat summary, progress widget * Fix issue with past games not showing ongoing ones * WIP question sets * More question set stuff * Improvements to OIDC experience and support for auto log in/out. * merge from next * Add favicon defaults. Update theme * Fix settings link bug, more thing * Theme stuff * Add settings link to user menu * Theme stuff * Revert "Theme stuff" This reverts commit a26c715cc3d8189bddcee6dadfa021eec98ab708. * Revert "Add settings link to user menu" This reverts commit d640ee80025f01185bee6c3a5918ebf8aba3b3af. * Revert "Theme stuff" This reverts commit c3e3b69b652224cac176dedebc0c66b8899cf490. * Revert "Fix settings link bug, more thing" This reverts commit f96dc596b83743e501fdf1c6b3a603fcda5e7ba3. * Revert "Add favicon defaults. Update theme" This reverts commit 1b2e5a7bfe8df33772e12c017d4d592b363ec6c8. * Rollback and reimplement oauth changes * Update oidc settings to match topo * Cleanup * Update dev env settings * Improvements to OIDC experience and support for auto log in/out. * Restore ux improvements * More retheme * Retheming and initial work on name improvements. * Update gh action versions * Retheming and name mgmt * Styling stuff * Styling stuff * Move challenge markdown * Un 'fix' start practice button * minor name fixes * Name cleanup * Finish merge from main --- .github/workflows/main.yml | 14 +- .../console/services/novnc-console.service.ts | 1 - .../admin-player-session.component.html | 8 +- .../src/app/admin/admin.module.ts | 19 +- .../challenge-browser.component.html | 2 +- .../challenge-browser.component.ts | 2 +- .../challenge-observer.component.html | 2 +- .../active-challenges-modal.component.html | 19 +- .../admin-enroll-team-modal.component.html | 2 +- .../admin-roles/admin-roles.component.html | 5 +- .../admin-roles/admin-roles.component.scss | 6 +- ...te-external-game-host-modal.component.html | 2 +- .../game-center-observe.component.html | 4 +- ...nter-practice-player-detail.component.scss | 2 +- .../game-center-settings.component.html | 20 +- .../game-center-team-detail.component.html | 4 +- .../game-center-teams.component.html | 16 +- .../game-center-teams.component.ts | 6 +- .../game-center/game-center.component.html | 4 +- .../game-map-editor.component.html | 2 +- .../game-map-editor.component.scss | 4 +- ...ge-manual-challenge-bonuses.component.scss | 2 +- .../support-auto-tag-admin.component.html | 2 +- .../admin/dashboard/dashboard.component.html | 2 +- .../game-mapper/game-mapper.component.html | 2 +- .../game-mapper/game-mapper.component.ts | 8 +- .../player-names/player-names.component.html | 3 +- .../practice-settings.component.html | 22 +- .../app/admin/prereqs/prereqs.component.html | 2 +- .../team-observer.component.html | 2 +- .../user-api-keys.component.html | 2 +- .../user-registrar.component.html | 62 ++--- .../user-registrar.component.scss | 25 +- .../user-registrar.component.ts | 38 +-- .../src/app/api/challenges.models.ts | 118 +++++---- .../src/app/api/challenges.service.ts | 91 +++---- projects/gameboard-ui/src/app/api/models.ts | 10 +- .../src/app/api/settings.models.ts | 3 + .../src/app/api/settings.service.ts | 14 ++ .../gameboard-ui/src/app/api/team.service.ts | 25 +- .../gameboard-ui/src/app/api/teams.models.ts | 4 + .../gameboard-ui/src/app/api/user-models.ts | 15 +- .../gameboard-ui/src/app/api/user.service.ts | 24 +- .../src/app/components/nav/nav.component.html | 34 ++- .../src/app/components/nav/nav.component.ts | 12 +- .../big-stat/big-stat.component.html | 4 +- .../big-stat/big-stat.component.scss | 6 +- .../challenge-solution-guide.component.html | 2 +- .../confirm-button.component.html | 6 +- .../confirm-button.component.ts | 4 +- .../dropzone/dropzone.component.html | 3 +- .../long-content-hider.component.scss | 2 +- .../modal-content.component.html | 6 +- .../modal/modal-confirm.component.ts | 4 +- .../multi-select/multi-select.component.html | 2 +- .../select-pager/select-pager.component.html | 10 +- .../ticket-label-picker.component.html | 2 +- .../ticket-list/ticket-list.component.html | 10 +- .../gameboard-ui/src/app/core/core.module.ts | 26 +- .../team-event-horizon.component.html | 2 +- .../certificate/certificate.component.html | 2 +- .../challenge-deploy-countdown.component.ts | 7 + .../game/components/play/play.component.html | 182 -------------- .../game/components/play/play.component.ts | 197 --------------- .../session-start-countdown.component.html | 2 +- .../gameboard-ui/src/app/game/game.module.ts | 21 +- .../gamespace-quiz.component.html | 2 +- .../gamespace-quiz.component.ts | 12 +- .../external-game-page.component.html | 2 +- .../gameboard-page.component.html | 7 +- .../gameboard-page.component.ts | 10 +- .../pipes/index-to-submitted-answers.pipe.ts | 4 +- .../player-presence.component.html | 2 +- .../gameboard-ui/src/app/home/home.module.ts | 12 +- .../practice-challenge-list.component.html | 13 +- .../practice-challenge-list.component.ts | 5 +- ...tice-challenge-solved-modal.component.html | 6 +- ...actice-challenge-solved-modal.component.ts | 38 +-- ...ice-challenge-state-summary.component.html | 33 +-- ...ctice-challenge-state-summary.component.ts | 81 ++---- .../practice-session.component.html | 26 +- .../practice-session.component.ts | 113 ++++----- .../gameboard-ui/src/app/prac/prac.module.ts | 12 + .../src/app/prac/practice.models.ts | 11 +- .../parameter-change-link.component.ts | 2 +- .../parameter-date-range.component.html | 6 +- .../parameter-sponsor.component.html | 8 +- ...er-challenge-attempts-modal.component.html | 2 +- ...eport-participation-summary.component.html | 2 +- .../report-card/report-card.component.html | 4 +- .../report-global-controls.component.html | 6 +- .../report-global-controls.component.scss | 6 +- .../report-stat-summary.component.html | 11 +- ...-sponsor-player-count-modal.component.html | 2 +- ...er-mode-performance-summary.component.html | 2 +- ...onsor-challenge-performance.component.html | 2 +- .../sort-header/sort-header.component.scss | 4 +- .../report-page/report-page.component.scss | 4 +- .../src/app/reports/reports.module.ts | 6 + ...coreboard-team-detail-modal.component.html | 2 +- ...coreboard-team-detail-modal.component.scss | 2 +- .../scoreboard/scoreboard.component.scss | 2 +- .../scoreboard/scoreboard.component.ts | 4 +- .../src/app/scoreboard/scoreboard.module.ts | 4 +- .../src/app/services/browser.service.ts | 26 -- .../challenge-questions-form.service.ts | 63 +++++ .../src/app/services/modal-confirm.service.ts | 6 +- .../src/app/services/practice.service.ts | 6 +- .../src/app/services/router.service.ts | 24 +- .../src/app/services/window.service.ts | 4 +- .../sponsor-edit-modal.component.html | 2 +- .../error-div/error-div.component.ts | 11 + .../hold-confirm-button.component.html | 3 + .../hold-confirm-button.component.scss | 0 .../hold-confirm-button.component.spec.ts | 23 ++ .../hold-confirm-button.component.ts | 41 ++++ .../components/spinner/spinner.component.ts | 11 +- .../core/pipes/camelspace.pipe.ts | 2 +- .../pipes/epoch-ms-to-ms-remaining.pipe.ts | 24 ++ .../pipes/epoch-ms-to-time-remaining.pipe.ts | 33 +++ .../core}/pipes/safe-url.pipe.ts | 2 +- .../core/pipes/to-support-code.pipe.ts | 2 +- .../challenge-questions.component.html | 163 +++++++++++++ .../challenge-questions.component.scss | 15 ++ .../challenge-questions.component.ts | 168 +++++++++++++ ...hallenge-submission-history.component.html | 36 +++ ...hallenge-submission-history.component.scss | 0 .../challenge-submission-history.component.ts | 36 +++ .../game-info-bubbles.component.ts | 108 ++++++++ .../games/components/play/play.component.html | 176 ++++++++++++++ .../components/play/play.component.scss | 7 +- .../games/components/play/play.component.ts | 166 +++++++++++++ .../components/vm-link/vm-link.component.ts | 33 +++ .../user-nav-item.component.html | 5 +- .../user-nav-item.component.scss | 4 + .../user-nav-item/user-nav-item.component.ts | 2 + .../src/app/stores/active-challenges.store.ts | 230 +++++++++++------- .../src/app/support/support.module.ts | 10 +- .../ticket-details.component.html | 8 +- .../ticket-details.component.scss | 4 +- .../admin-system-notifications.component.html | 4 +- ...t-system-notification-modal.component.html | 4 +- .../certificate-printer.component.html | 2 +- .../competitive-certificates.component.html | 2 +- .../practice-certificates.component.html | 2 +- .../profile-editor.component.html | 74 +++--- .../profile-editor.component.ts | 99 ++++---- .../profile-history.component.ts | 1 + .../settings/settings.component.html | 2 +- .../src/app/users/users.module.ts | 4 + .../src/app/utility/auth.service.ts | 5 + .../imagestack/imagestack.component.html | 5 - .../imagestack/imagestack.component.scss | 25 -- .../imagestack/imagestack.component.ts | 31 --- .../components/login/login.component.html | 2 +- .../morphing-text.component.html | 17 -- .../morphing-text.component.scss | 43 ---- .../morphing-text/morphing-text.component.ts | 125 ---------- .../src/app/utility/config.service.ts | 6 +- .../src/app/utility/pipes/untag.pipe.ts | 16 -- .../src/app/utility/utility.module.ts | 32 +-- .../src/environments/environment.ts | 2 +- projects/gameboard-ui/src/scss/_toastify.scss | 4 +- projects/gameboard-ui/src/styles.scss | 27 +- 164 files changed, 2090 insertions(+), 1534 deletions(-) create mode 100644 projects/gameboard-ui/src/app/api/settings.models.ts create mode 100644 projects/gameboard-ui/src/app/api/settings.service.ts delete mode 100644 projects/gameboard-ui/src/app/game/components/play/play.component.html delete mode 100644 projects/gameboard-ui/src/app/game/components/play/play.component.ts delete mode 100644 projects/gameboard-ui/src/app/services/browser.service.ts create mode 100644 projects/gameboard-ui/src/app/services/challenge-questions-form.service.ts rename projects/gameboard-ui/src/app/{ => standalone}/core/components/error-div/error-div.component.ts (70%) create mode 100644 projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.html create mode 100644 projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.scss create mode 100644 projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.spec.ts create mode 100644 projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.ts rename projects/gameboard-ui/src/app/{ => standalone}/core/components/spinner/spinner.component.ts (90%) rename projects/gameboard-ui/src/app/{ => standalone}/core/pipes/camelspace.pipe.ts (89%) create mode 100644 projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-ms-remaining.pipe.ts create mode 100644 projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-time-remaining.pipe.ts rename projects/gameboard-ui/src/app/{utility => standalone/core}/pipes/safe-url.pipe.ts (89%) rename projects/gameboard-ui/src/app/{ => standalone}/core/pipes/to-support-code.pipe.ts (86%) create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.html create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.scss create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.html create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.scss create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.ts create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/game-info-bubbles/game-info-bubbles.component.ts create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html rename projects/gameboard-ui/src/app/{game => standalone/games}/components/play/play.component.scss (68%) create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts create mode 100644 projects/gameboard-ui/src/app/standalone/games/components/vm-link/vm-link.component.ts delete mode 100644 projects/gameboard-ui/src/app/utility/components/imagestack/imagestack.component.html delete mode 100644 projects/gameboard-ui/src/app/utility/components/imagestack/imagestack.component.scss delete mode 100644 projects/gameboard-ui/src/app/utility/components/imagestack/imagestack.component.ts delete mode 100644 projects/gameboard-ui/src/app/utility/components/morphing-text/morphing-text.component.html delete mode 100644 projects/gameboard-ui/src/app/utility/components/morphing-text/morphing-text.component.scss delete mode 100644 projects/gameboard-ui/src/app/utility/components/morphing-text/morphing-text.component.ts delete mode 100644 projects/gameboard-ui/src/app/utility/pipes/untag.pipe.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cba55766..6faa48c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: release: - types: [ "published" ] + types: ["published"] push: branches: - dev @@ -15,32 +15,32 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Docker meta id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + uses: docker/metadata-action@v5 with: images: cmusei/gameboard-ui - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: fetch wmks run: curl https://box.cmusei.dev/wmks-jam-v1.tar -s -o wmks.tar - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile diff --git a/projects/gameboard-mks/src/app/components/console/services/novnc-console.service.ts b/projects/gameboard-mks/src/app/components/console/services/novnc-console.service.ts index 73addf77..b1fe77e8 100644 --- a/projects/gameboard-mks/src/app/components/console/services/novnc-console.service.ts +++ b/projects/gameboard-mks/src/app/components/console/services/novnc-console.service.ts @@ -65,7 +65,6 @@ export class NoVNCConsoleService implements ConsoleService { copy(): void { } async paste(text: string): Promise { - console.log(text); this.client.clipboardPasteFrom(text); } diff --git a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html index 6ea8eadb..7a6de1d6 100644 --- a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html +++ b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html @@ -42,7 +42,7 @@

Session Extension

-
@@ -69,11 +69,11 @@

Other tools

- - Unenroll diff --git a/projects/gameboard-ui/src/app/admin/admin.module.ts b/projects/gameboard-ui/src/app/admin/admin.module.ts index d8c651f6..f1f96274 100644 --- a/projects/gameboard-ui/src/app/admin/admin.module.ts +++ b/projects/gameboard-ui/src/app/admin/admin.module.ts @@ -73,6 +73,7 @@ import { PlayerSponsorReportComponent } from './player-sponsor-report/player-spo import { PracticeSettingsComponent } from './practice/practice-settings/practice-settings.component'; import { PracticeComponent } from './practice/practice.component'; import { PrereqsComponent } from './prereqs/prereqs.component'; +import { SafeUrlPipe } from '@/standalone/core/pipes/safe-url.pipe'; import { SpecBrowserComponent } from './spec-browser/spec-browser.component'; import { SponsorBrowserComponent } from './sponsor-browser/sponsor-browser.component'; import { SupportReportLegacyComponent } from './support-report-legacy/support-report-legacy.component'; @@ -80,10 +81,14 @@ import { TeamObserverComponent } from './team-observer/team-observer.component'; import { UserApiKeysComponent } from './user-api-keys/user-api-keys.component'; import { UserRegistrarComponent } from './user-registrar/user-registrar.component'; import { UserReportComponent } from './user-report/user-report.component'; -import { GameInfoBubblesComponent } from "../standalone/components/game-info-bubbles/game-info-bubbles.component"; +import { GameInfoBubblesComponent } from "../standalone/games/components/game-info-bubbles/game-info-bubbles.component"; import { ScoreboardComponent } from '@/scoreboard/components/scoreboard/scoreboard.component'; import { GameIdResolver } from './resolvers/game-id.resolver'; import { GameCenterSelectedTabResolver } from './resolvers/game-center-selected-tab-resolver'; +import { ErrorDivComponent } from '@/standalone/core/components/error-div/error-div.component'; +import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; +import { ToSupportCodePipe } from '@/standalone/core/pipes/to-support-code.pipe'; +import { IfHasPermissionDirective } from '@/standalone/directives/if-has-permission.directive'; @NgModule({ declarations: [ @@ -151,7 +156,7 @@ import { GameCenterSelectedTabResolver } from './resolvers/game-center-selected- TeamListCardComponent, GameCenterTeamDetailComponent, TeamCenterComponent, - GameMapEditorComponent, + GameMapEditorComponent ], imports: [ CommonModule, @@ -217,7 +222,15 @@ import { GameCenterSelectedTabResolver } from './resolvers/game-center-selected- ScoreboardModule, SponsorsModule, SystemNotificationsModule, - GameInfoBubblesComponent + + // standalones + ErrorDivComponent, + GameInfoBubblesComponent, + IfHasPermissionDirective, + SafeUrlPipe, + SpinnerComponent, + ToSupportCodePipe, ] }) + export class AdminModule { } diff --git a/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.html b/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.html index f1e93f7e..3edfe164 100644 --- a/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.html +++ b/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.html @@ -56,7 +56,7 @@

Challenges

- diff --git a/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.ts b/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.ts index 9b02cbac..d59e2915 100644 --- a/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.ts +++ b/projects/gameboard-ui/src/app/admin/challenge-browser/challenge-browser.component.ts @@ -103,7 +103,7 @@ export class ChallengeBrowserComponent { this.isLoadingSubmissions = true; try { - const submissions = await firstValueFrom(this.challengesService.getSubmissions(c.id)); + const submissions = await firstValueFrom(this.challengesService.getSubmissionsLegacy(c.id)); this.selectedAudit = submissions.submittedAnswers.map(s => { return { submittedOn: s.submittedOn, answers: s.answers.map(a => a || "(no response)") }; }); diff --git a/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html b/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html index 9ce2e4e0..c441c389 100644 --- a/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html +++ b/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html @@ -116,7 +116,7 @@

Consoles

diff --git a/projects/gameboard-ui/src/app/admin/components/active-challenges-modal/active-challenges-modal.component.html b/projects/gameboard-ui/src/app/admin/components/active-challenges-modal/active-challenges-modal.component.html index d8ac41e6..e74b07f1 100644 --- a/projects/gameboard-ui/src/app/admin/components/active-challenges-modal/active-challenges-modal.component.html +++ b/projects/gameboard-ui/src/app/admin/components/active-challenges-modal/active-challenges-modal.component.html @@ -37,7 +37,7 @@
{{ challenge.team.name }}
- @@ -48,8 +48,9 @@
Session End: - {{ challenge.team.session.end | datetimeToDate | friendlyDateAndTime - }} + + {{ challenge.team.session.end | datetimeToDate | friendlyDateAndTime }} +
@@ -64,18 +65,18 @@
+ class="btn btn-success" tooltip="View this game" placement="bottom"> @@ -101,7 +102,7 @@
diff --git a/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html b/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html index fcd47f0a..476b2347 100644 --- a/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html +++ b/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html @@ -19,7 +19,7 @@ [avatarTooltip]="user.sponsor.name" (actionClick)="handleSelectedActionClick(user.id)" actionTooltip="Remove this player">
{{ user.name }}
- diff --git a/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.html b/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.html index 16cd45d4..18318a58 100644 --- a/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.html +++ b/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.html @@ -1,7 +1,10 @@

Permissions

-
Your role is {{ appPermissionsOverview.yourRole | titlecase }}. +
Your role is + + {{ appPermissionsOverview.yourRole | titlecase }} + .
diff --git a/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.scss b/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.scss index eb970267..bacaa4e2 100644 --- a/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.scss +++ b/projects/gameboard-ui/src/app/admin/components/admin-roles/admin-roles.component.scss @@ -17,7 +17,7 @@ } .has-permission { - color: $info; + color: $success; } .no-permission { @@ -25,7 +25,7 @@ } .your-role { - background-color: $info; + background-color: $success; .has-permission { color: $foreground; @@ -37,7 +37,7 @@ } .sticky-header { - top: 60px; + top: 50px; left: 0px; position: sticky; z-index: 999; diff --git a/projects/gameboard-ui/src/app/admin/components/delete-external-game-host-modal/delete-external-game-host-modal.component.html b/projects/gameboard-ui/src/app/admin/components/delete-external-game-host-modal/delete-external-game-host-modal.component.html index 2aa5bf7b..9684fe78 100644 --- a/projects/gameboard-ui/src/app/admin/components/delete-external-game-host-modal/delete-external-game-host-modal.component.html +++ b/projects/gameboard-ui/src/app/admin/components/delete-external-game-host-modal/delete-external-game-host-modal.component.html @@ -19,5 +19,5 @@ - Loading the host... + Loading the game host... diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html index 705b64d5..9c661e52 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html @@ -1,7 +1,7 @@
- -
diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss index 6a5c733b..8d1692f6 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss @@ -1,7 +1,7 @@ @import "../../../../../scss/variables"; pre code { - background-color: $info; + background-color: $success; border-radius: 4px; color: #fff; padding: 4px; diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html index c9507e9e..338aa3d9 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html @@ -20,7 +20,7 @@
-
+
diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts b/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts index 81c8bfe8..c6d59857 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-list/practice-challenge-list.component.ts @@ -20,7 +20,6 @@ import { AuthService } from '@/utility/auth.service'; }) export class PracticeChallengeListComponent { list$: Observable; - search$ = new BehaviorSubject({}); appname = ''; faSearch = faSearch; @@ -96,9 +95,7 @@ export class PracticeChallengeListComponent { paged(s: number): void { this.routerService.updateQueryParams({ - parameters: { - skip: s, navigate: true - } + parameters: { skip: s, navigate: true } }); } diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.html b/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.html index 27fc7306..09d70617 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.html +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.html @@ -17,9 +17,9 @@

Congratulations!

@@ -37,6 +37,6 @@

Congratulations!

diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.ts b/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.ts index cafa6c3d..e58ab77b 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.ts +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-solved-modal/practice-challenge-solved-modal.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { BoardPlayer } from '@/api/board-models'; import { BoardService } from '@/api/board.service'; -import { LocalActiveChallenge } from '@/api/challenges.models'; +import { UserActiveChallenge } from '@/api/challenges.models'; import { PlayerMode } from '@/api/player-models'; import { MiniBoardSpec } from '@/core/components/feedback-form/feedback-form.component'; import { PracticeService } from '@/services/practice.service'; @@ -11,7 +11,7 @@ import { UnsubscriberService } from '@/services/unsubscriber.service'; import { BsModalRef } from 'ngx-bootstrap/modal'; export interface PracticeChallengeSolvedModalContext { - challenge: LocalActiveChallenge; + challenge: UserActiveChallenge; } interface LegacyFeedbackFormContext { @@ -29,7 +29,7 @@ export class PracticeChallengeSolvedModalComponent implements OnInit { context?: PracticeChallengeSolvedModalContext; protected certificateUrl?: string; protected isCertificateConfigured = false; - protected feedbackFormContext?: LegacyFeedbackFormContext = undefined; + // protected feedbackFormContext?: LegacyFeedbackFormContext = undefined; constructor( private boardService: BoardService, @@ -48,7 +48,7 @@ export class PracticeChallengeSolvedModalComponent implements OnInit { this.certificateUrl = this.routerService.getCertificatePrintableUrl(PlayerMode.practice, this.context.challenge.spec.id); // load feedback form data - this.feedbackFormContext = await this.loadFeedbackFormContext(this.context.challenge); + // this.feedbackFormContext = await this.loadFeedbackFormContext(this.context.challenge); // wire up event handler for background-click dismiss if (this.modalRef.onHidden) { @@ -63,21 +63,21 @@ export class PracticeChallengeSolvedModalComponent implements OnInit { this.routerService.toPracticeArea(); } - private async loadFeedbackFormContext(challenge: LocalActiveChallenge): Promise { - const player = await firstValueFrom(this.boardService.load(challenge.player.id)); + // private async loadFeedbackFormContext(challenge: UserActiveChallenge): Promise { + // const player = await firstValueFrom(this.boardService.load(challenge.player.id)); - if (!player.game.feedbackTemplate?.challenge?.length) - return undefined; + // if (!player.game.feedbackTemplate?.challenge?.length) + // return undefined; - return { - boardPlayer: player, - boardSpec: { - id: challenge.spec.id, - instance: { - id: challenge.challengeDeployment.challengeId, - state: { isActive: challenge.challengeDeployment.isDeployed }, - } - } - }; - } + // return { + // boardPlayer: player, + // boardSpec: { + // id: challenge.spec.id, + // instance: { + // id: challenge.challengeDeployment.challengeId, + // state: { isActive: challenge.challengeDeployment.isDeployed }, + // } + // } + // }; + // } } diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.html b/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.html index 4d5fa381..c0e11368 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.html +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.html @@ -1,15 +1,14 @@ - + {{ isChangingSessionEnd ? "Updating your session..." : "Finding your challenge..." }} - + @@ -17,41 +16,35 @@
-
+
+ -
{{c.gameName}}
- + +
{{ c.gameName }}
+
- - - - - - +
ScoreCumulative Time Time Remaining
{{userActivePracticeChallenge.scoreAndAttemptsState.score}} - {{ (msElapsed$ | async) || 0 | clock }} - - {{ (msRemaining$ | async) || undefined | countdown }} + {{activeChallenge.scoreAndAttemptsState.score}} + {{ activeChallenge.endsAt | epochMsToTimeRemainingString | async }}
- +
- + Extend Session - + End Session
diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts b/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts index 11d90bc9..17a6cfa6 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts @@ -1,16 +1,13 @@ -import { LocalActiveChallenge } from '@/api/challenges.models'; -import { fa } from '@/services/font-awesome.service'; -import { LogService } from '@/services/log.service'; -import { UserService as LocalUserService } from '@/utility/user.service'; -import { UnsubscriberService } from '@/services/unsubscriber.service'; import { Component } from '@angular/core'; import { DateTime } from 'luxon'; -import { Observable, combineLatest, map, tap, timer } from 'rxjs'; +import { Observable, of } from 'rxjs'; +import { UserActiveChallenge } from '@/api/challenges.models'; +import { fa } from '@/services/font-awesome.service'; +import { UnsubscriberService } from '@/services/unsubscriber.service'; import { ActiveChallengesRepo } from '@/stores/active-challenges.store'; import { slug } from '@/../tools/functions'; import { TeamService } from '@/api/team.service'; import { ToastService } from '@/utility/services/toast.service'; -import { NotificationService } from '@/services/notification.service'; @Component({ selector: 'app-practice-challenge-state-summary', @@ -19,71 +16,25 @@ import { NotificationService } from '@/services/notification.service'; providers: [UnsubscriberService] }) export class PracticeChallengeStateSummaryComponent { + protected activeChallenge$: Observable; + protected msPerHour = 3600000; + protected msRemaining$ = of(0); protected extendTooltip = ""; protected isChangingSessionEnd = false; - protected msElapsed$?: Observable; - protected msRemaining$?: Observable; - protected msInAnHour = 3600000; - protected userActivePracticeChallenge: LocalActiveChallenge | undefined | null; protected fa = fa; protected slug = slug; - private _timer$ = timer(0, 1000); constructor( activeChallengesRepo: ActiveChallengesRepo, - localUserService: LocalUserService, - private logService: LogService, - private notificationService: NotificationService, private teamService: TeamService, - private toastService: ToastService, - // have to keep "unsub" around so it gets ngDestroyed. - // this is an argument for an inherited base class, i think - private unsub: UnsubscriberService) { - unsub.add( - combineLatest([ - localUserService.user$, - activeChallengesRepo.activePracticeChallenge$ - ]).pipe( - map(([localUser, practiceChallenge]) => ({ - localUser, - practiceChallenge - })), - ).subscribe(ctx => { - this.updatePracticeChallenge(ctx.practiceChallenge); - this.isChangingSessionEnd = false; - }) - ); - } - - private async updatePracticeChallenge(challenge: LocalActiveChallenge | null) { - // store the active challenge - this.userActivePracticeChallenge = challenge || null; - - if (challenge?.teamId) { - await this.notificationService.init(challenge.teamId); - this.logService.logInfo("Practice challenge notification hub: connected to hub", challenge); - } - - // update timers to accurately reflect the active challenge - this.msElapsed$ = this._timer$.pipe( - map(() => - this.userActivePracticeChallenge?.session.start ? - DateTime.now().diff(this.userActivePracticeChallenge.session.start).toMillis() : - undefined - ) - ); - - this.msRemaining$ = this._timer$.pipe( - map(() => this.userActivePracticeChallenge?.session.end ? this.userActivePracticeChallenge.session.end.diffNow().toMillis() : 0), - tap(msRemaining => this.extendTooltip = this.getExtendTooltip(msRemaining)) - ); + private toastService: ToastService) { + this.activeChallenge$ = activeChallengesRepo.activePracticeChallenge$; } - async extendSession(practiceChallenge: LocalActiveChallenge): Promise { + async extendSession(practiceChallenge: UserActiveChallenge): Promise { this.isChangingSessionEnd = true; - const teamId = practiceChallenge.teamId; await this.teamService.extendSession({ - teamId, + teamId: practiceChallenge.team.id, sessionEnd: new Date() }); @@ -91,18 +42,14 @@ export class PracticeChallengeStateSummaryComponent { this.showExtensionToast(DateTime.now().plus({ minutes: 60 })); } - async endSession(practiceChallenge: LocalActiveChallenge): Promise { - if (!this.userActivePracticeChallenge) { - this.logService.logError("Can't extend a session without an active practice challenge."); - } - + async endSession(practiceChallenge: UserActiveChallenge): Promise { this.isChangingSessionEnd = true; - await this.teamService.endSession({ teamId: this.userActivePracticeChallenge!.teamId }); + await this.teamService.endSession({ teamId: practiceChallenge.team.id }); this.isChangingSessionEnd = false; } private getExtendTooltip(msRemaining: number) { - if (msRemaining < this.msInAnHour) { + if (msRemaining < this.msPerHour) { return "If you want more time to practice, you can extend your session. Sessions extend to a maximum remaining time of 60 minutes."; } diff --git a/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html b/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html index b226bdea..0bfddebb 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html +++ b/projects/gameboard-ui/src/app/prac/components/practice-session/practice-session.component.html @@ -1,10 +1,10 @@
-
diff --git a/projects/gameboard-ui/src/app/reports/components/player-challenge-attempts-modal/player-challenge-attempts-modal.component.html b/projects/gameboard-ui/src/app/reports/components/player-challenge-attempts-modal/player-challenge-attempts-modal.component.html index fe175d4a..55a3469d 100644 --- a/projects/gameboard-ui/src/app/reports/components/player-challenge-attempts-modal/player-challenge-attempts-modal.component.html +++ b/projects/gameboard-ui/src/app/reports/components/player-challenge-attempts-modal/player-challenge-attempts-modal.component.html @@ -19,6 +19,6 @@
{{ context.subtitleDetail [primaryData]="'date'">
diff --git a/projects/gameboard-ui/src/app/reports/components/players-report-participation-summary/players-report-participation-summary.component.html b/projects/gameboard-ui/src/app/reports/components/players-report-participation-summary/players-report-participation-summary.component.html index eaccd0a9..d377f883 100644 --- a/projects/gameboard-ui/src/app/reports/components/players-report-participation-summary/players-report-participation-summary.component.html +++ b/projects/gameboard-ui/src/app/reports/components/players-report-participation-summary/players-report-participation-summary.component.html @@ -42,6 +42,6 @@

Games

diff --git a/projects/gameboard-ui/src/app/reports/components/report-card/report-card.component.html b/projects/gameboard-ui/src/app/reports/components/report-card/report-card.component.html index 4fd6e1a1..82b4cace 100644 --- a/projects/gameboard-ui/src/app/reports/components/report-card/report-card.component.html +++ b/projects/gameboard-ui/src/app/reports/components/report-card/report-card.component.html @@ -13,7 +13,7 @@

Fields

@@ -23,7 +23,7 @@

Filters

diff --git a/projects/gameboard-ui/src/app/reports/components/report-global-controls/report-global-controls.component.html b/projects/gameboard-ui/src/app/reports/components/report-global-controls/report-global-controls.component.html index 782f759a..2e45e1cd 100644 --- a/projects/gameboard-ui/src/app/reports/components/report-global-controls/report-global-controls.component.html +++ b/projects/gameboard-ui/src/app/reports/components/report-global-controls/report-global-controls.component.html @@ -1,12 +1,14 @@
- [ About report filters ] + + [ About report filters ] +
-
+
diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/player-mode-performance-summary/player-mode-performance-summary.component.html b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/player-mode-performance-summary/player-mode-performance-summary.component.html index 30f5ec73..198fb391 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/player-mode-performance-summary/player-mode-performance-summary.component.html +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/player-mode-performance-summary/player-mode-performance-summary.component.html @@ -18,7 +18,7 @@

{{ (context.isPractice ? "Practice" : "Competitive")}} Mode

*ngFor="let challenge of context.summary.challenges"> diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html index 76b47cf6..6a764c24 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html @@ -91,6 +91,6 @@

Sponsor Performance

diff --git a/projects/gameboard-ui/src/app/reports/components/sort-header/sort-header.component.scss b/projects/gameboard-ui/src/app/reports/components/sort-header/sort-header.component.scss index 35a6f979..0c7dd9c7 100644 --- a/projects/gameboard-ui/src/app/reports/components/sort-header/sort-header.component.scss +++ b/projects/gameboard-ui/src/app/reports/components/sort-header/sort-header.component.scss @@ -2,11 +2,11 @@ .component-container { background-color: unset; - color: $info; + color: $success; text-decoration: dotted; &.active { - background-color: $info; + background-color: $success; color: $foreground; } } diff --git a/projects/gameboard-ui/src/app/reports/pages/report-page/report-page.component.scss b/projects/gameboard-ui/src/app/reports/pages/report-page/report-page.component.scss index d291ec1c..fb045d57 100644 --- a/projects/gameboard-ui/src/app/reports/pages/report-page/report-page.component.scss +++ b/projects/gameboard-ui/src/app/reports/pages/report-page/report-page.component.scss @@ -68,8 +68,8 @@ $subtleSize: 0.9rem; } .tooltipped-value { - border-bottom: dotted 1px $info; - color: $info; + border-bottom: dotted 1px $success; + color: $success; cursor: pointer; font-weight: bold; } diff --git a/projects/gameboard-ui/src/app/reports/reports.module.ts b/projects/gameboard-ui/src/app/reports/reports.module.ts index 02a5cf96..a8a47138 100644 --- a/projects/gameboard-ui/src/app/reports/reports.module.ts +++ b/projects/gameboard-ui/src/app/reports/reports.module.ts @@ -52,6 +52,8 @@ import { SiteUsageReportChallengesListComponent } from './components/reports/sit import { SortHeaderComponent } from './components/sort-header/sort-header.component'; import { SpecQuestionPerformanceModalComponent } from './components/spec-question-performance-modal/spec-question-performance-modal.component'; import { FeedbackGameReportComponent } from './components/reports/feedback-game-report/feedback-game-report.component'; +import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; +import { ErrorDivComponent } from '@/standalone/core/components/error-div/error-div.component'; @NgModule({ declarations: [ @@ -124,6 +126,10 @@ import { FeedbackGameReportComponent } from './components/reports/feedback-game- ]), FontAwesomeModule, CoreModule, + + // standalones, + ErrorDivComponent, + SpinnerComponent ], providers: [UnsubscriberService] }) diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html index 0eecadb0..a48a0ce8 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.html @@ -161,7 +161,7 @@

Additional Bonuses

diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss index a92b89fa..b2ed5272 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.scss @@ -79,7 +79,7 @@ tfoot td { } .grand-total-container { - background-color: $info; + background-color: $success; color: #eee; font-weight: bold; text-transform: uppercase; diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.scss b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.scss index 0100f944..6b3c8b46 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.scss +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.scss @@ -18,7 +18,7 @@ thead { } .qualifying-message-row { - background-color: $info; + background-color: $success; color: #eee; font-weight: bold; text-align: center; diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts index 2f82e4e8..d1d6192f 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts @@ -1,10 +1,10 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Subscription, firstValueFrom, interval, map } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription, firstValueFrom, interval } from 'rxjs'; import { ScoringService } from '@/services/scoring/scoring.service'; import { ScoreboardData, ScoreboardDataTeam } from '@/services/scoring/scoring.models'; import { ModalConfirmService } from '@/services/modal-confirm.service'; import { ScoreboardTeamDetailModalComponent } from '../scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; -import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-scoreboard', diff --git a/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts b/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts index db16e49f..18e12569 100644 --- a/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts +++ b/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts @@ -6,6 +6,7 @@ import { ChallengeBonusesToTooltip } from './pipes/challenge-bonuses-to-tooltip' import { ScoreboardComponent } from './components/scoreboard/scoreboard.component'; import { ScoreboardTeamDetailModalComponent } from './components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; import { ScoreToTooltipPipe } from './pipes/score-to-tooltip.pipe'; +import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; const PUBLIC_DECLARATIONS = [ ScoreboardComponent, @@ -20,7 +21,8 @@ const PUBLIC_DECLARATIONS = [ ], imports: [ CommonModule, - CoreModule + CoreModule, + SpinnerComponent ], exports: PUBLIC_DECLARATIONS }) diff --git a/projects/gameboard-ui/src/app/services/browser.service.ts b/projects/gameboard-ui/src/app/services/browser.service.ts deleted file mode 100644 index d3f828fa..00000000 --- a/projects/gameboard-ui/src/app/services/browser.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable } from '@angular/core'; - -export interface TabRef { - url: string; - window: Window | null; -} - -@Injectable({ providedIn: 'root' }) -export class BrowserService { - private _tabs: TabRef[] = []; - - showTab(url: string): void { - let item = this._tabs.find(t => t.url === url); - - if (!item) { - item = { url, window: null }; - this._tabs.push(item); - } - - if (!item.window || item.window.closed) { - item.window = window.open(url); - } else { - item.window.focus(); - } - } -} diff --git a/projects/gameboard-ui/src/app/services/challenge-questions-form.service.ts b/projects/gameboard-ui/src/app/services/challenge-questions-form.service.ts new file mode 100644 index 00000000..2dabf54e --- /dev/null +++ b/projects/gameboard-ui/src/app/services/challenge-questions-form.service.ts @@ -0,0 +1,63 @@ +import { SectionSubmission } from '@/api/board-models'; +import { ChallengeProgressViewSection } from '@/api/challenges.models'; +import { Injectable } from '@angular/core'; +import { AbstractControl, AbstractControlOptions, FormControl, FormGroup } from '@angular/forms'; + +@Injectable({ providedIn: 'root' }) +export class ChallengeQuestionsFormService { + private specialKeys = ["challengeId", "sectionIndex"]; + + buildSectionForm(challengeId: string, sectionIndex: number, section: ChallengeProgressViewSection): FormGroup { + const controls: { [key: string]: FormControl } = {}; + controls["challengeId"] = new FormControl({ value: challengeId, disabled: true }); + controls["sectionIndex"] = new FormControl({ value: sectionIndex, disabled: true }); + + for (let i = 0; i < section.questions.length; i++) { + controls[this.buildQuestionFormName(sectionIndex, i)] = new FormControl({ + value: section.questions[i].isCorrect ? section.questions[i].answer : "", + disabled: section.questions[i].isCorrect + }); + } + + return new FormGroup(controls, { + // this is really stupid, but i really don't get this API + validators: (control: AbstractControl) => { + const questionControls = this.getQuestionControls(control as FormGroup); + + if (questionControls.some(ctrl => ctrl.value && !ctrl.disabled)) + return null; + + return ["At least once new answer is required for submission"]; + } + }); + } + + buildQuestionFormName(sectionIndex: number, questionIndex: number) { + return `s${sectionIndex}q${questionIndex}`; + } + + getSubmission(form: AbstractControl): SectionSubmission { + const questionControls = this.getQuestionControls(form as FormGroup); + const challengeId = form.get("challengeId")?.value; + const sectionIndex = form.get("sectionIndex")?.value; + + if (!challengeId) { + throw new Error(`Couldn't resolve challengeId for form: ${JSON.stringify(form)}`); + } + + if (sectionIndex === undefined || sectionIndex === null) { + throw new Error(`Couldn't resolve sectionIndex for form: ${JSON.stringify(form)}`); + } + + return { + id: challengeId, + sectionIndex: sectionIndex, + questions: questionControls.map(ctrl => ({ answer: ctrl.value })) + }; + } + + private getQuestionControls(formGroup: FormGroup): AbstractControl[] { + const controlKeys = Object.keys(formGroup.controls).filter(keyName => this.specialKeys.indexOf(keyName) < 0); + return controlKeys.map(key => formGroup.controls[key] as AbstractControl); + } +} diff --git a/projects/gameboard-ui/src/app/services/modal-confirm.service.ts b/projects/gameboard-ui/src/app/services/modal-confirm.service.ts index 352cf806..1cb09ac2 100644 --- a/projects/gameboard-ui/src/app/services/modal-confirm.service.ts +++ b/projects/gameboard-ui/src/app/services/modal-confirm.service.ts @@ -1,4 +1,4 @@ -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable, OnDestroy, TemplateRef } from '@angular/core'; import { BsModalRef, BsModalService, ModalOptions } from 'ngx-bootstrap/modal'; import { Subscription } from 'rxjs'; import { ModalConfirmComponent } from '@/core/components/modal/modal-confirm.component'; @@ -33,6 +33,10 @@ export class ModalConfirmService implements OnDestroy { this.bsModalRef = this.openWithDefaultStyles(config); } + openTemplate(templateRef: TemplateRef) { + this.bsModalService.show(templateRef, { class: "modal-dialog-centered" }); + } + hide(isCancelEvent = false): void { if (!isCancelEvent) { this.hiddenSub?.unsubscribe(); diff --git a/projects/gameboard-ui/src/app/services/practice.service.ts b/projects/gameboard-ui/src/app/services/practice.service.ts index 391718ab..efdd8eff 100644 --- a/projects/gameboard-ui/src/app/services/practice.service.ts +++ b/projects/gameboard-ui/src/app/services/practice.service.ts @@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, firstValueFrom, map } from 'rxjs'; import { ApiUrlService } from './api-url.service'; -import { PracticeModeSettings, SearchPracticeChallengesResult } from '@/prac/practice.models'; +import { PracticeModeSettings, PracticeSession, SearchPracticeChallengesResult } from '@/prac/practice.models'; import { LogService } from './log.service'; import { GameCardContext } from '@/api/game-models'; @@ -24,6 +24,10 @@ export class PracticeService { await this.updateIsEnabled(); } + async getSession(): Promise { + return firstValueFrom(this.http.get(this.apiUrl.build("practice/session")).pipe(map(s => s || null))); + } + getSettings(): Observable { return this.http.get(this.apiUrl.build("practice/settings")); } diff --git a/projects/gameboard-ui/src/app/services/router.service.ts b/projects/gameboard-ui/src/app/services/router.service.ts index b5493f82..536f3a65 100644 --- a/projects/gameboard-ui/src/app/services/router.service.ts +++ b/projects/gameboard-ui/src/app/services/router.service.ts @@ -2,15 +2,13 @@ import { Injectable, OnDestroy } from '@angular/core'; import { NavigationEnd, ActivatedRoute, Params, Router, UrlTree } from '@angular/router'; import { Subscription, filter } from 'rxjs'; import { ReportKey } from '@/reports/reports-models'; -import { BrowserService } from './browser.service'; -import { ObjectService } from './object.service'; -import { VmState } from '@/api/board-models'; import { PlayerMode } from '@/api/player-models'; import { ConfigService } from '@/utility/config.service'; import { UserService as LocalUser } from '@/utility/user.service'; import { slug } from "@/../tools/functions"; import { GameCenterTab } from '@/admin/components/game-center/game-center.models'; -import { WindowService } from './window.service'; +import { SimpleEntity } from '@/api/models'; +import { ObjectService } from './object.service'; export interface QueryParamsUpdate { parameters?: Params, @@ -22,13 +20,11 @@ export class RouterService implements OnDestroy { private _navEndSub?: Subscription; constructor( - private browser: BrowserService, private config: ConfigService, private localUser: LocalUser, - public route: ActivatedRoute, - private router: Router, private objectService: ObjectService, - private windowService: WindowService) { } + public route: ActivatedRoute, + private router: Router) { } public getCurrentPathBase(): string { const urlTree = this.router.parseUrl(this.router.url); @@ -144,16 +140,12 @@ export class RouterService implements OnDestroy { return this.router.navigateByUrl(this.router.parseUrl(`/support/tickets/${highlightTicketKey}`)); } - public buildVmConsoleUrl(vm: VmState, isPractice = false) { - if (!vm || !vm.isolationId) { - throw new Error(`Can't launch a VM console without an isolationId.`); + public buildVmConsoleUrl(challengeId: string, vm: SimpleEntity, isPractice = false) { + if (!vm || !challengeId) { + throw new Error(`Can't launch a VM console without a challengeId.`); } - return `${this.config.mkshost}?f=1&s=${vm.isolationId}&v=${vm.name || 'Console'}${isPractice ? "&l=true" : ""}`; - } - - public toVmConsole(vm: VmState) { - this.browser.showTab(this.buildVmConsoleUrl(vm)); + return `${this.config.mkshost}?f=1&s=${challengeId}&v=${vm.name || 'Console'}${isPractice ? "&l=true" : ""}`; } public deleteQueryParams(): Promise { diff --git a/projects/gameboard-ui/src/app/services/window.service.ts b/projects/gameboard-ui/src/app/services/window.service.ts index f0d70c72..9b7ac62b 100644 --- a/projects/gameboard-ui/src/app/services/window.service.ts +++ b/projects/gameboard-ui/src/app/services/window.service.ts @@ -23,8 +23,8 @@ export class WindowService implements OnDestroy { return this.document.defaultView!; } - open() { - this.document.defaultView?.open(undefined, "_blank"); + open(url: string) { + this.document.defaultView?.open(url, "_blank"); } print() { diff --git a/projects/gameboard-ui/src/app/sponsors/components/sponsor-edit-modal/sponsor-edit-modal.component.html b/projects/gameboard-ui/src/app/sponsors/components/sponsor-edit-modal/sponsor-edit-modal.component.html index 9a4aa2fe..fd659bc4 100644 --- a/projects/gameboard-ui/src/app/sponsors/components/sponsor-edit-modal/sponsor-edit-modal.component.html +++ b/projects/gameboard-ui/src/app/sponsors/components/sponsor-edit-modal/sponsor-edit-modal.component.html @@ -9,6 +9,6 @@

Edit Sponsor "{{ editingSponsor.name }}"

diff --git a/projects/gameboard-ui/src/app/core/components/error-div/error-div.component.ts b/projects/gameboard-ui/src/app/standalone/core/components/error-div/error-div.component.ts similarity index 70% rename from projects/gameboard-ui/src/app/core/components/error-div/error-div.component.ts rename to projects/gameboard-ui/src/app/standalone/core/components/error-div/error-div.component.ts index b8ce5001..d6731931 100644 --- a/projects/gameboard-ui/src/app/core/components/error-div/error-div.component.ts +++ b/projects/gameboard-ui/src/app/standalone/core/components/error-div/error-div.component.ts @@ -1,10 +1,21 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +import { CommonModule } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MarkdownModule } from 'ngx-markdown'; +import { CamelspacePipe } from '../../pipes/camelspace.pipe'; +import { AlertModule } from 'ngx-bootstrap/alert'; @Component({ selector: 'app-error-div', + standalone: true, + imports: [ + CommonModule, + AlertModule, + MarkdownModule, + CamelspacePipe + ], template: ` {{e.error?.message || e.message || e | camelspace}} diff --git a/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.html b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.html new file mode 100644 index 00000000..cf3fe718 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.html @@ -0,0 +1,3 @@ + diff --git a/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.scss b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.spec.ts b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.spec.ts new file mode 100644 index 00000000..d10a9318 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HoldConfirmButtonComponent } from './hold-confirm-button.component'; + +describe('HoldConfirmButtonComponent', () => { + let component: HoldConfirmButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ HoldConfirmButtonComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HoldConfirmButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.ts b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.ts new file mode 100644 index 00000000..50a8ba64 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/components/hold-confirm-button/hold-confirm-button.component.ts @@ -0,0 +1,41 @@ +import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { fromEvent, interval, switchMap, takeUntil, takeWhile, tap, timer } from 'rxjs'; +import { UnsubscriberService } from '@/services/unsubscriber.service'; + +@Component({ + selector: 'app-hold-confirm-button', + standalone: true, + imports: [CommonModule], + templateUrl: './hold-confirm-button.component.html', + styleUrls: ['./hold-confirm-button.component.scss'] +}) +export class HoldConfirmButtonComponent implements AfterViewInit { + @Input() confirmTimeMs = 1500; + @Output() click = new EventEmitter(); + + @ViewChild("button") buttonRef?: ElementRef; + + constructor(private unsub: UnsubscriberService) { } + + ngAfterViewInit(): void { + if (!this.buttonRef?.nativeElement) + throw new Error("Couldn't resolve the button."); + + const button = this.buttonRef.nativeElement; + const mouseDown$ = fromEvent(button, 'mousedown'); + const mouseUp$ = fromEvent(button, 'mouseup'); + const mouseLeave$ = fromEvent(button, 'mouseleave'); + + this.unsub.add( + mouseDown$.pipe( + switchMap(() => + timer(1500).pipe( + takeUntil(mouseUp$), + takeUntil(mouseLeave$) + ) + ) + ).subscribe(() => this.click.emit(button)) + ); + } +} diff --git a/projects/gameboard-ui/src/app/core/components/spinner/spinner.component.ts b/projects/gameboard-ui/src/app/standalone/core/components/spinner/spinner.component.ts similarity index 90% rename from projects/gameboard-ui/src/app/core/components/spinner/spinner.component.ts rename to projects/gameboard-ui/src/app/standalone/core/components/spinner/spinner.component.ts index 8b2f1d25..b0652798 100644 --- a/projects/gameboard-ui/src/app/core/components/spinner/spinner.component.ts +++ b/projects/gameboard-ui/src/app/standalone/core/components/spinner/spinner.component.ts @@ -1,10 +1,13 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; @Component({ selector: 'app-spinner', + standalone: true, + imports: [CommonModule], template: `
@@ -22,7 +25,7 @@ import { Component, Input } from '@angular/core'; - + - + `, styles: [ - ".spinner-component { width: 100%; text-align: center; }", + ".spinner-component { width: 100%; text-align: center; margin: 0 auto; }", "h1 { font-size: 0.85rem; font-weight: bold; text-transform: uppercase; }" ], }) export class SpinnerComponent { - @Input() color?: string; + @Input() color?: string = "#41ad57"; @Input() textPosition: "top" | "bottom" = "top"; } diff --git a/projects/gameboard-ui/src/app/core/pipes/camelspace.pipe.ts b/projects/gameboard-ui/src/app/standalone/core/pipes/camelspace.pipe.ts similarity index 89% rename from projects/gameboard-ui/src/app/core/pipes/camelspace.pipe.ts rename to projects/gameboard-ui/src/app/standalone/core/pipes/camelspace.pipe.ts index 81eee27f..5ef66785 100644 --- a/projects/gameboard-ui/src/app/core/pipes/camelspace.pipe.ts +++ b/projects/gameboard-ui/src/app/standalone/core/pipes/camelspace.pipe.ts @@ -3,7 +3,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -@Pipe({ name: 'camelspace' }) +@Pipe({ name: 'camelspace', standalone: true }) export class CamelspacePipe implements PipeTransform { transform(value: string, ...args: unknown[]): unknown { return value.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); diff --git a/projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-ms-remaining.pipe.ts b/projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-ms-remaining.pipe.ts new file mode 100644 index 00000000..8a9506b1 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-ms-remaining.pipe.ts @@ -0,0 +1,24 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DateTime } from 'luxon'; +import { interval, map, Observable } from 'rxjs'; + +@Pipe({ name: 'epochMsToMsRemaining', standalone: true }) +export class EpochMsToMsRemainingPipe implements PipeTransform { + + transform(value: number | null | undefined): Observable | null { + if (value === null || value === undefined || value < 0) { + return null; + } + + const dateTime = DateTime.fromMillis(value); + + return interval(1000).pipe(map(() => { + const diffMs = dateTime.diffNow().toMillis(); + + if (diffMs <= 0) + return null; + + return diffMs; + })); + } +} diff --git a/projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-time-remaining.pipe.ts b/projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-time-remaining.pipe.ts new file mode 100644 index 00000000..8ff4bff4 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/core/pipes/epoch-ms-to-time-remaining.pipe.ts @@ -0,0 +1,33 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DateTime } from 'luxon'; +import { interval, map, Observable, of } from 'rxjs'; + +@Pipe({ + name: 'epochMsToTimeRemainingString', + standalone: true +}) +export class EpochMsToTimeRemainingStringPipe implements PipeTransform { + transform(value: number | null | undefined): Observable | null { + if (value === null || value === undefined || value < 0) { + return of("--"); + } + + const dateTime = DateTime.fromMillis(value); + + return interval(1000).pipe(map(() => { + const diff = dateTime.diffNow(); + + if (dateTime.toMillis() <= 0) + return "--"; + + if (diff.as("hours") > 1) + return diff + .shiftTo("hours", "minutes") + .toHuman({ maximumFractionDigits: 0 }); + + return diff + .shiftTo("minutes", "seconds") + .toFormat("mm:ss"); + })); + } +} diff --git a/projects/gameboard-ui/src/app/utility/pipes/safe-url.pipe.ts b/projects/gameboard-ui/src/app/standalone/core/pipes/safe-url.pipe.ts similarity index 89% rename from projects/gameboard-ui/src/app/utility/pipes/safe-url.pipe.ts rename to projects/gameboard-ui/src/app/standalone/core/pipes/safe-url.pipe.ts index e8b1116c..d8c2bac5 100644 --- a/projects/gameboard-ui/src/app/utility/pipes/safe-url.pipe.ts +++ b/projects/gameboard-ui/src/app/standalone/core/pipes/safe-url.pipe.ts @@ -1,7 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -@Pipe({ name: 'safeurl' }) +@Pipe({ name: 'safeUrl', standalone: true }) export class SafeUrlPipe implements PipeTransform { constructor(protected sanitizer: DomSanitizer) { } diff --git a/projects/gameboard-ui/src/app/core/pipes/to-support-code.pipe.ts b/projects/gameboard-ui/src/app/standalone/core/pipes/to-support-code.pipe.ts similarity index 86% rename from projects/gameboard-ui/src/app/core/pipes/to-support-code.pipe.ts rename to projects/gameboard-ui/src/app/standalone/core/pipes/to-support-code.pipe.ts index 6dd51a6d..1355c113 100644 --- a/projects/gameboard-ui/src/app/core/pipes/to-support-code.pipe.ts +++ b/projects/gameboard-ui/src/app/standalone/core/pipes/to-support-code.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core'; -@Pipe({ name: 'toSupportCode' }) +@Pipe({ name: 'toSupportCode', standalone: true }) export class ToSupportCodePipe implements PipeTransform { transform(value: { id: string, tag?: string }): string { diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.html b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.html new file mode 100644 index 00000000..a5bb823e --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.html @@ -0,0 +1,163 @@ +
+ + + + +
+ + + Loading your challenge... + + + + + + + + + This challenge has multiple sections. To unlock the next one, you'll need to + + +
    +
  • + Score at least {{ progress!.nextSectionPreReqThisSection }} points on the + current section. +
  • +
  • + Score at least {{ progress!.nextSectionPreReqTotal }} points total. + (You currently have {{ progress!.score }} points.) +
  • +
+
+ + + score at least {{ progress?.nextSectionPreReqThisSection }} points on the + current section. + + + + score at least {{ progress!.nextSectionPreReqTotal }} points total. + (You currently have {{ progress!.score }} points.) + +
+ + + + +
+ {{ (size === "compact" ? "S" : "Section ") + sectionTab.friendlyIndex }} +
+ + +
+ {{ sectionTab.section.name }} +
+
+ Unavailable +
+
+
+ + +
+
+
+ + +

{{ context.section.name || "Section " + context.index + 1 }}

+ + +

+ You've completed this section. Great job! + + + Onto the + + next + ? + +

+
+ +
+ +
+ +
+
    +
  • +
    +
    +
    + Q{{ questionIndex + 1 }} +
    + +
    + + + + ({{ question.scoreMax }} points) + +
    +
    + +
    +
    + +
    + + +
    +
    +
  • +
+ +
+
+
+ Remaining: + {{ endsAt$ | async | epochMsToTimeRemainingString | async }} + + ({{progress.maxAttempts - progress.attempts}} attempts) + +
+ +
+ Score: + {{ progress.score || 0 }} / {{ progress.maxPoints }} +
+
+
+
+ {{ size === "compact" ? "Previous Submissions" : "See my previous submissions" }} +
+ + Submit My Answers + +
+
+
+ + +
+ This section isn't available yet - check back when you've answered a few more questions! +
+
+ + + + + + diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.scss b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.scss new file mode 100644 index 00000000..5b9baa4f --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.scss @@ -0,0 +1,15 @@ +@import "../../../../../scss/variables"; + +.q-label { + border-right: solid 2px $success; + font-size: 2.8rem; + line-height: 2.6rem; +} + +.form-group label { + font-size: 1.1rem !important; +} + +.challenge-status-container { + border-top: dashed 1px #aaa; +} diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts new file mode 100644 index 00000000..952ee592 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/challenge-questions/challenge-questions.component.ts @@ -0,0 +1,168 @@ +import { Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { firstValueFrom, map } from 'rxjs'; +import { ChallengeProgressView, ChallengeProgressViewSection, UserActiveChallenge } from '@/api/challenges.models'; +import { ChallengesService } from '@/api/challenges.service'; +import { CoreModule } from '@/core/core.module'; +import { fa } from '@/services/font-awesome.service'; +import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; +import { HoldConfirmButtonComponent } from "@/standalone/core/components/hold-confirm-button/hold-confirm-button.component"; +import { ToastService } from '@/utility/services/toast.service'; +import { AbstractControl, FormGroup } from '@angular/forms'; +import { ChallengeQuestionsFormService } from '@/services/challenge-questions-form.service'; +import { EpochMsToTimeRemainingStringPipe } from '@/standalone/core/pipes/epoch-ms-to-time-remaining.pipe'; +import { ModalConfirmService } from '@/services/modal-confirm.service'; +import { ChallengeSubmissionHistoryComponent } from "../challenge-submission-history/challenge-submission-history.component"; +import { SimpleEntity } from '@/api/models'; +import { ActiveChallengesRepo } from '@/stores/active-challenges.store'; + +interface SectionTabViewModel { + index: number; + friendlyIndex: number; + questionsRemaining?: number; + isAvailable: boolean; + section?: ChallengeProgressViewSection; + form?: FormGroup; +} + +@Component({ + selector: 'app-challenge-questions', + standalone: true, + imports: [ + CommonModule, + CoreModule, + EpochMsToTimeRemainingStringPipe, + SpinnerComponent, + HoldConfirmButtonComponent, + ChallengeSubmissionHistoryComponent + ], + providers: [ActiveChallengesRepo, ModalConfirmService], + templateUrl: "./challenge-questions.component.html", + styleUrls: ["./challenge-questions.component.scss"] +}) +export class ChallengeQuestionsComponent implements OnChanges { + @Input() challengeId?: string; + @Input() size: "normal" | "compact" = "normal"; + + protected endsAt$ = this.activeChallengesRepo.activePracticeChallenge$.pipe(map(c => c?.endsAt)); + protected fa = fa; + protected hasSubmissionHistory = false; + protected isGrading = false; + protected progress?: ChallengeProgressView; + protected sectionTabs: SectionTabViewModel[] = []; + protected selectedSectionIndex = 0; + protected spec?: SimpleEntity; + protected team?: SimpleEntity; + + @ViewChild('submissionHistoryModal') protected submissionHistoryModalTemplate?: TemplateRef; + + constructor( + private activeChallengesRepo: ActiveChallengesRepo, + private challengeService: ChallengesService, + private formService: ChallengeQuestionsFormService, + private modalConfirmService: ModalConfirmService, + private toastService: ToastService) { + } + + async ngOnChanges(changes: SimpleChanges) { + if (!changes.challengeId) + return; + + if (!this.challengeId) { + this.progress = undefined; + return; + } + + await this.load(this.challengeId); + } + + protected handleSectionSelect(sectionIndex: number) { + this.selectedSectionIndex = sectionIndex; + + if (!this.progress) { + return; + } + } + + protected async handleSubmit(formData: AbstractControl) { + this.isGrading = true; + const presubmitScore = this.progress?.score || 0; + await firstValueFrom(this.challengeService.grade(this.formService.getSubmission(formData))); + await this.load(this.challengeId!); + this.isGrading = false; + + if (!this.progress) + return; + + if (this.progress.score === presubmitScore && this.progress.score < this.progress.maxPoints) { + // let's be encouraging, shall we? + this.toastService.showMessage("Not quite... keep trying!"); + } + + if (this.progress.score >= this.progress.maxPoints) { + this.toastService.showMessage("Whoa, nice! You've **completely solved** this challenge. Well done!"); + } + else if (this.progress.score > presubmitScore) { + this.toastService.showMessage(`Nailed it! Your score has increased to **${this.progress?.score}**.`); + } + } + + protected handleViewSubmissionHistory(challengeId: string) { + if (!this.submissionHistoryModalTemplate) { + throw new Error("Couldn't resolve submission history template."); + } + + this.modalConfirmService.openTemplate(this.submissionHistoryModalTemplate); + } + + private async load(challengeId: string) { + const response = await this.challengeService.getProgress(challengeId); + this.progress = response.progress; + + // grab the team and spec for display purposes + this.spec = response.spec; + this.team = response.team; + + if (!this.progress) { + this.sectionTabs = []; + return; + } + + // some challenges have multiple sections, so we need to present a tabbed interface with a tab for + // each. However, sections can also have prerequisities, and if they do, the game engine won't send + // down question data until those are met. We still want to hint that they're coming by showing a disabled + // tab, so track how many total sections there are in a range we can iterate on + this.sectionTabs = [...Array(this.progress.variant.totalSectionCount).keys()].map(sectionIndex => { + if (!this.challengeId) { + throw new Error("ChallengeId is required"); + } + + const isAvailable = !!this.progress && this.progress.variant.sections.length > sectionIndex; + if (!isAvailable) { + return { + friendlyIndex: sectionIndex + 1, + index: sectionIndex, + isAvailable: false, + }; + } + + const section = this.progress!.variant.sections[sectionIndex]; + + return { + friendlyIndex: sectionIndex + 1, + index: sectionIndex, + isAvailable: isAvailable, + isMultiSection: this.progress!.variant.totalSectionCount > 1, + questionsRemaining: section.questions.filter(q => !q.isCorrect).length, + section: section, + form: this.formService.buildSectionForm(this.challengeId, sectionIndex, section) + }; + }); + + // select the "last" tab with unanswered questions + this.handleSectionSelect(this.progress.variant.sections.length - 1); + + // find out if any submissions have been made (decides the availability of the "submission history" modal) + this.hasSubmissionHistory = this.progress.attempts > 0; + } +} diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.html b/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.html new file mode 100644 index 00000000..2bad6e40 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + +
SectionSubmissions
{{ +entry.key + 1 }} +
    +
  • +
    + {{ submission.submittedAt | epochMsToDateTime | friendlyDateAndTime }} +
    +
      +
    1. {{ answer }}
    2. +
    +
  • +
+
+
+ + + Loading submissions... + diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.scss b/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.ts new file mode 100644 index 00000000..5f4b0004 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/challenge-submission-history/challenge-submission-history.component.ts @@ -0,0 +1,36 @@ +import { Component, inject, Input, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ChallengesService } from '@/api/challenges.service'; +import { ChallengeSubmissionHistory } from '@/api/challenges.models'; +import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; +import { Challenge } from '@/api/board-models'; +import { firstValueFrom } from 'rxjs'; +import { CoreModule } from '@/core/core.module'; + +@Component({ + selector: 'app-challenge-submission-history', + standalone: true, + imports: [ + CommonModule, + CoreModule, + SpinnerComponent + ], + templateUrl: './challenge-submission-history.component.html', + styleUrls: ['./challenge-submission-history.component.scss'] +}) +export class ChallengeSubmissionHistoryComponent implements OnInit { + @Input() challengeId?: string; + private _challengeService = inject(ChallengesService); + + protected challenge?: Challenge; + protected submissionHistory?: ChallengeSubmissionHistory; + + async ngOnInit() { + if (!this.challengeId) { + throw new Error("ChallengeId is required"); + } + + this.challenge = await firstValueFrom(this._challengeService.retrieve(this.challengeId)); + this.submissionHistory = await this._challengeService.getSubmissions(this.challengeId); + } +} diff --git a/projects/gameboard-ui/src/app/standalone/games/components/game-info-bubbles/game-info-bubbles.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/game-info-bubbles/game-info-bubbles.component.ts new file mode 100644 index 00000000..8e71ea5b --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/game-info-bubbles/game-info-bubbles.component.ts @@ -0,0 +1,108 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DateTime } from 'luxon'; +import { GameEngineMode } from '@/api/game-models'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { fa } from "@/services/font-awesome.service"; +import { SizeProp } from '@fortawesome/fontawesome-svg-core'; + +export interface GameInfoBubbleProperties { + name: string; + season: string; + competition: string; + track: string; + + isLive: boolean; + isPublished: boolean; + maxTeamSize: number; + mode: GameEngineMode; + modeUrl: string; + startDate?: DateTime +} + +@Component({ + selector: 'app-game-info-bubbles', + standalone: true, + imports: [ + CommonModule, + FontAwesomeModule, + TooltipModule + ], + styles: [ + "fa-icon { vertical-align: middle; }", + "li { margin-right: 1rem; padding: 0; text-align: center; vertical-align: middle; }", + "img { object-fit: contain }", + ".team-icon path { stroke: #00ffff; fill: #00ffff } " + ], + template: ` +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+ + +
{{ infoTooltip.name }}
+
{{ infoTooltip.detail }}
+
+ ` +}) +export class GameInfoBubblesComponent implements OnChanges { + @Input() game?: GameInfoBubbleProperties; + @Input() bubbleSize: SizeProp = "2xl"; + + protected engineModeIcon = fa.computer; + protected engineModeTooltip = ""; + protected fa = fa; + protected infoTooltip: { name: string; detail?: string; } = { name: "" }; + protected teamSizeTooltip = ""; + protected teamSizeIcon = fa.person; + + ngOnChanges(changes: SimpleChanges): void { + if (!changes.game?.currentValue) + return; + + const game = changes.game.currentValue; + + this.engineModeIcon = game.mode == GameEngineMode.Standard ? fa.computer : fa.gamepad; + this.engineModeTooltip = game.mode == GameEngineMode.Standard ? "Standard VMs" : "External Host"; + + this.infoTooltip = { + name: game.name, + detail: `${this.buildTooltipDetail(game)}` + }; + + this.teamSizeIcon = game.maxTeamSize > 1 ? fa.peopleGroup : fa.circleUser; + this.teamSizeTooltip = game.maxTeamSize > 1 ? `Teams of up to ${game.maxTeamSize}` : "Individual"; + } + + private buildTooltipDetail(game: GameInfoBubbleProperties): string { + let detail = ""; + + if (game.competition) + detail += `${game.competition} / `; + + if (game.season) + detail += `${game.season} / `; + + if (game.track) + detail += `${game.track} / `; + + return detail ? detail.substring(0, detail.length - 3) : detail; + } +} diff --git a/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html new file mode 100644 index 00000000..59db2301 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.html @@ -0,0 +1,176 @@ + + +
+
+ + + + + + Stopping challenge resources... + + + + +

Check out the new challenge resources panel!

+ +

+ Click here + to "sticky" the Challenge panel so it's always visible as you scroll down the page. + (You can return the panel to the bottom of the page at any time by clicking the appropriate link + in the panel.) +

+
+ +

+ +

+ + + + +

+
+
    +
  • + +
  • +
+
+ +
+ + + Destroy + + + + Deploy + +
+ +
+

Challenge Questions

+ +
+ +
+ +
+
+ +
+
+ Support Code: + + {{ { id: challenge.id } | toSupportCode }} + +
+ +
+ Having trouble? + + + Create Ticket + +
+
+
+
+
+ +
+ +
+
+
+ + + + + +
+ +
+
    +
  • + +
  • +
+
+ +
+ + + Destroy + + + + Deploy + +
+ +
+
Questions
+ +
+ +
+ +
+
Need help?
+ + +
+ +
+ +
+ Click + here + to deactivate the sticky Challenge panel +
+
+
+ + + Loading your challenge... + + + + Challenge {{ "Console" | pluralizer:vmUrls }} + + + + +

Solution Guide

+ +

+ Having trouble? We've created a step-by-step solution guide for this challenge. If you get + stuck, you can find it here. +

+
+
diff --git a/projects/gameboard-ui/src/app/game/components/play/play.component.scss b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.scss similarity index 68% rename from projects/gameboard-ui/src/app/game/components/play/play.component.scss rename to projects/gameboard-ui/src/app/standalone/games/components/play/play.component.scss index 96d67eab..b598cc87 100644 --- a/projects/gameboard-ui/src/app/game/components/play/play.component.scss +++ b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.scss @@ -1,16 +1,17 @@ -@import "../../../../scss/variables"; +@import "../../../../../scss/variables"; .mini-player-container { background-color: $gray-900; border-radius: 6px; - border: solid 1px $info; + border: solid 1px $success; font-size: 0.95rem; margin-left: 24px; margin-top: 80px; padding: 1rem; position: sticky; top: 50px; - min-width: 380px; + min-width: 420px; + width: 420px; h5 { color: $foreground; diff --git a/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts new file mode 100644 index 00000000..70d5c433 --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/play/play.component.ts @@ -0,0 +1,166 @@ +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { firstValueFrom, Observable, tap } from 'rxjs'; +import { fa } from "@/services/font-awesome.service"; +import { RouterService } from '@/services/router.service'; +import { ChallengesService } from '@/api/challenges.service'; +import { ChallengeSolutionGuide, UserActiveChallenge } from '@/api/challenges.models'; +import { PlayerMode } from '@/api/player-models'; +import { ActiveChallengesRepo } from '@/stores/active-challenges.store'; +import { UnsubscriberService } from '@/services/unsubscriber.service'; +import { SpecSummary } from '@/api/spec-models'; +import { WindowService } from '@/services/window.service'; +import { LocalStorageService, StorageKey } from '@/services/local-storage.service'; +import { CommonModule } from '@angular/common'; +import { SpinnerComponent } from '@/standalone/core/components/spinner/spinner.component'; +import { CoreModule } from '@/core/core.module'; +import { ToSupportCodePipe } from '@/standalone/core/pipes/to-support-code.pipe'; +import { ChallengeQuestionsComponent } from "../challenge-questions/challenge-questions.component"; +import { UtilityModule } from '@/utility/utility.module'; +import { ChallengeDeployCountdownComponent } from '@/game/components/challenge-deploy-countdown/challenge-deploy-countdown.component'; +import { ErrorDivComponent } from '@/standalone/core/components/error-div/error-div.component'; +import { VmLinkComponent } from "../vm-link/vm-link.component"; +import { UserService } from '@/utility/user.service'; + +@Component({ + selector: 'app-play', + standalone: true, + imports: [ + CommonModule, + CoreModule, + ChallengeDeployCountdownComponent, + ErrorDivComponent, + SpinnerComponent, + ToSupportCodePipe, + ChallengeQuestionsComponent, + UtilityModule, + VmLinkComponent + ], + templateUrl: './play.component.html', + styleUrls: ['./play.component.scss'], + providers: [UnsubscriberService] +}) +export class PlayComponent implements OnChanges { + @Input() autoPlay = false; + @Input() challengeSpec: SpecSummary | null = null; + @Input() playerId?: string; + @Output() challengeStarted = new EventEmitter(); + @Output() deployStatusChanged = new EventEmitter(); + + protected challenge: UserActiveChallenge | null = null; + protected errors: any[] = []; + protected fa = fa; + protected isDeploying = false; + protected isMiniPlayerAvailable = false; + protected isMiniPlayerSelected = false; + protected isUndeploying = false; + protected showMiniPlayerPrompt = false; + protected solutionGuide: ChallengeSolutionGuide | null = null; + protected vmUrls: { [id: string]: string } = {}; + protected windowWidth$: Observable; + + constructor( + windowService: WindowService, + private activeChallengesRepo: ActiveChallengesRepo, + private challengesService: ChallengesService, + private localStorage: LocalStorageService, + private localUser: UserService, + private routerService: RouterService, + private unsub: UnsubscriberService) { + this.windowWidth$ = windowService.resize$; + this.unsub.add( + windowService.resize$.subscribe(width => { + this.isMiniPlayerAvailable = width >= 1140; + this.isMiniPlayerSelected = this.localStorage.get(StorageKey.UsePlayPane) === "true"; + this.showMiniPlayerPrompt = this.localStorage.get(StorageKey.UsePlayPane) === null; + + if (!this.isMiniPlayerAvailable && this.isMiniPlayerSelected) + this.toggleMiniPlayer(); + }) + ); + } + + public async ngOnChanges(changes: SimpleChanges) { + if (!(changes.autoPlay || changes.challengeSpec || changes.teamId)) + return; + + if (this.autoPlay && this.playerId && this.challengeSpec && this.challengeSpec.id !== changes.challengeSpec?.currentValue) { + await this.deployChallenge({ challengeSpecId: changes.challengeSpec.currentValue.id, playerId: this.playerId }); + } + } + + protected async deployVms(challengeId: string) { + if (!challengeId) { + throw new Error("Can't deploy from the Play component without a challenge."); + } + + this.isDeploying = true; + this.deployStatusChanged.emit(true); + await firstValueFrom(this.challengesService.deploy({ id: challengeId })); + this.isDeploying = false; + this.deployStatusChanged.emit(false); + } + + protected async undeployVms(challengeId: string) { + this.isUndeploying = true; + await firstValueFrom(this.challengesService.undeploy({ id: challengeId })); + this.isUndeploying = false; + } + + protected toggleMiniPlayer() { + this.showMiniPlayerPrompt = false; + if (this.isMiniPlayerAvailable) { + this.isMiniPlayerSelected = !this.isMiniPlayerSelected; + this.localStorage.add(StorageKey.UsePlayPane, this.isMiniPlayerSelected); + + } + else { + this.isMiniPlayerSelected = false; + this.localStorage.add(StorageKey.UsePlayPane, false); + } + } + + private buildVmLinks(challenge: UserActiveChallenge | null) { + const vmUrls: { [id: string]: string } = {}; + + if (!challenge) + return vmUrls; + + for (const vm of challenge.vms) { + vmUrls[vm.id] = this.routerService.buildVmConsoleUrl(challenge.id, vm, challenge.mode === PlayerMode.practice); + } + + return vmUrls; + } + + private async deployChallenge(args: { challengeSpecId: string, playerId: string }) { + if (!this.localUser.user$.value) { + throw new Error("Can't deploy for an unauthed user."); + } + + this.errors = []; + this.solutionGuide = null; + + this.deployStatusChanged.emit(true); + this.isDeploying = true; + + try { + const startedChallenge = await firstValueFrom(this.challengesService.startPlaying({ + specId: args.challengeSpecId, + playerId: args.playerId, + userId: this.localUser.user$.value.id + })); + + this.challenge = this.activeChallengesRepo.getActivePracticeChallenge(); + this.vmUrls = this.buildVmLinks(this.challenge); + + // also look up the solution guide if there is one + this.solutionGuide = await firstValueFrom(this.challengesService.getSolutionGuide(startedChallenge.id)); + } + catch (err: any) { + this.errors.push(err); + } + + this.isDeploying = false; + this.deployStatusChanged.emit(false); + } +} diff --git a/projects/gameboard-ui/src/app/standalone/games/components/vm-link/vm-link.component.ts b/projects/gameboard-ui/src/app/standalone/games/components/vm-link/vm-link.component.ts new file mode 100644 index 00000000..d65506ee --- /dev/null +++ b/projects/gameboard-ui/src/app/standalone/games/components/vm-link/vm-link.component.ts @@ -0,0 +1,33 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { fa } from '@/services/font-awesome.service'; +import { SafeUrlPipe } from '@/standalone/core/pipes/safe-url.pipe'; + +@Component({ + selector: 'app-vm-link', + standalone: true, + imports: [ + CommonModule, + FontAwesomeModule, + SafeUrlPipe + ], + styles: [ + "fa-icon { border-right: solid 1px #fff; padding: 0px 8px }", + "a { border: solid 1px #fff }", + "a:hover { color: #41ad57; border: solid 1px #41ad57; }", + "a:hover fa-icon { border-right: solid 1px #41ad57; }" + ], + template: ` + + +
{{vm.name}}
+
+`, +}) +export class VmLinkComponent { + @Input() vm?: { name: string, url: string }; + + protected fa = fa; +} diff --git a/projects/gameboard-ui/src/app/standalone/user/components/user-nav-item/user-nav-item.component.html b/projects/gameboard-ui/src/app/standalone/user/components/user-nav-item/user-nav-item.component.html index 131c0595..64b6c936 100644 --- a/projects/gameboard-ui/src/app/standalone/user/components/user-nav-item/user-nav-item.component.html +++ b/projects/gameboard-ui/src/app/standalone/user/components/user-nav-item/user-nav-item.component.html @@ -12,7 +12,7 @@
- + @@ -137,9 +139,9 @@

- -
diff --git a/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.scss b/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.scss index ac6de55d..bf3fe200 100644 --- a/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.scss +++ b/projects/gameboard-ui/src/app/support/ticket-details/ticket-details.component.scss @@ -124,11 +124,11 @@ textarea { opacity: 20%; } -.btn-info:disabled { +.btn-success:disabled { background-color: rgb(108, 117, 125); border-color: rgb(108, 117, 125); } .copy-id-button { - color: $info; + color: $success; } diff --git a/projects/gameboard-ui/src/app/system-notifications/components/admin-system-notifications/admin-system-notifications.component.html b/projects/gameboard-ui/src/app/system-notifications/components/admin-system-notifications/admin-system-notifications.component.html index 9b61849c..6a382744 100644 --- a/projects/gameboard-ui/src/app/system-notifications/components/admin-system-notifications/admin-system-notifications.component.html +++ b/projects/gameboard-ui/src/app/system-notifications/components/admin-system-notifications/admin-system-notifications.component.html @@ -9,7 +9,7 @@

Notifications

-
@@ -70,7 +70,7 @@

Notifications

- Delete diff --git a/projects/gameboard-ui/src/app/system-notifications/components/create-edit-system-notification-modal/create-edit-system-notification-modal.component.html b/projects/gameboard-ui/src/app/system-notifications/components/create-edit-system-notification-modal/create-edit-system-notification-modal.component.html index 7a9cbbe8..9c8a6e99 100644 --- a/projects/gameboard-ui/src/app/system-notifications/components/create-edit-system-notification-modal/create-edit-system-notification-modal.component.html +++ b/projects/gameboard-ui/src/app/system-notifications/components/create-edit-system-notification-modal/create-edit-system-notification-modal.component.html @@ -52,8 +52,8 @@