Skip to content

Commit

Permalink
Improve game organization and management (#34)
Browse files Browse the repository at this point in the history
* Game settings input suggestions with datalists
* Toggle game table view in admin dashboard
* Display landing page cards grouped by month/year
* Fix admin card view sizing bug
* Add padding to admin header
* Group upcoming games by month/year as well
* Landing page hover info on game cards
* Basic search by term on landing page
* Go to game lobby from admin table
* Use open/close registration terminology for card hover
* Remove displayed metadata next to game title
* Enhance game info tile format
* Timezone for game info panel
* Tooltips imported and added to dashboard icons
* Max width for info panel when unauth'd
* Placeholder text for admin game search
* Shorten save delay in admin settings & add spinner to dashboard
  • Loading branch information
sei-jbritton authored Jun 15, 2022
1 parent 25ccafd commit f60d708
Show file tree
Hide file tree
Showing 25 changed files with 646 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -->

<div class="mb-4">
<h1 class="mb-0">Administration</h1>
<h1 class="admin-header mb-0">Administration</h1>
</div>

<main class="container mb-4 pb-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
a {
font-size: 1.3rem;
}
.admin-header {
padding-top: 1rem;
padding-left: 2rem;
}
6 changes: 5 additions & 1 deletion projects/gameboard-ui/src/app/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { GameDesignerComponent } from './game-designer/game-designer.component';
import { UserRegistrarComponent } from './user-registrar/user-registrar.component';
import { UtilityModule } from '../utility/utility.module';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ButtonsModule } from 'ngx-bootstrap/buttons';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ApiModule } from '../api/api.module';
import { FormsModule } from '@angular/forms';
import { GameEditorComponent } from './game-editor/game-editor.component';
Expand Down Expand Up @@ -78,7 +80,9 @@ import { FeedbackReportComponent } from './feedback-report/feedback-report.compo
ApiModule,
UtilityModule,
FontAwesomeModule,
ButtonsModule
TooltipModule,
ButtonsModule,
BsDropdownModule
]
})
export class AdminModule { }
227 changes: 180 additions & 47 deletions projects/gameboard-ui/src/app/admin/dashboard/dashboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,65 +16,192 @@
<!-- game browser -->
<div class="row">

<div class="input-group mb-4 col-12">
<div class="input-group mb-2 col-12">
<div class="input-group-prepend">
<span id="search-label" class="input-group-text">Search</span>
</div>
<input id="search-input" type="search" class="form-control" [(ngModel)]="search.term" (input)="typing($event)"
aria-label="search term" aria-describedby="search-label">
<input id="search-input" type="search" autocomplete="off" class="form-control border-0" [(ngModel)]="search.term" (input)="typing($event)"
aria-label="search term" aria-describedby="search-label" placeholder="Enter name, season, track, division, series, sponsor, key, mode, or id">
</div>

<!-- <div class=""> -->

<div class="dropzone col-3">
<app-dropzone class="" (dropped)="dropped($event)">
<pre>Drag in a yaml game array or</pre>
<button class="btn btn-secondary btn-sm" (click)="create()">
<fa-icon [icon]="faPlus"></fa-icon>
<span>New Game</span>
</button>
</app-dropzone>
</div>

<div class="wrapper col-3" *ngFor="let g of games$ | async; trackBy:trackById" (mouseenter)="on(g)"
(mouseleave)="off(g)" (focus)="on(g)" tabindex="0">

<app-game-card [game]="g" class="col-lg-3 col-md-4 col-6 mb-4"></app-game-card>

<div class="overlay d-flex flex-column justify-content-center align-items-center" *ngIf="hot===g">
<a class="btn btn-outline-info my-2" [routerLink]="['../registrar', g.id]">
<fa-icon [icon]="faUsers"></fa-icon>
<span>Players</span>
</a>
<a class="btn btn-outline-info my-2" [routerLink]="['../observer/challenges', g.id]">
<fa-icon [icon]="faTv"></fa-icon>
<span>Observe</span>
</a>
<a class="btn btn-outline-info my-2" [routerLink]="['../designer', g.id]">
<fa-icon [icon]="faCog"></fa-icon>
<span>Settings</span>
</a>
<div>
<button class="btn btn-outline-info btn-sm mx-1" (click)="clone(g)">
<fa-icon [icon]="faCopy"></fa-icon>
<span>Clone</span>
</button>
<button class="btn btn-outline-info btn-sm mx-1" (click)="yaml(g)">
<fa-icon [icon]="faCopy"></fa-icon>
<span>yaml</span>
</button>
<button class="btn btn-outline-info btn-sm mx-1" (click)="json(g)">
<fa-icon [icon]="faCopy"></fa-icon>
<span>json</span>
</button>

<div *ngIf="tableView" class="col-4">
<button class="btn btn-secondary btn-sm" (click)="create()">
<fa-icon [icon]="faPlus"></fa-icon>
<span>New Game</span>
</button>
</div>
<div class="col-4 my-0 py-0" [class]="tableView ? 'offset-4' : 'offset-8'">
<div class="pb-0 pt-1 my-0 py-0">
<div class="text-white float-right auto-h d-flex align-items-center my-0 py-0">
<label class="py-0 my-0">Cards</label>
<label class="btn text-info my-0 py-0" btnCheckbox id="isPublished-input"
name="isPublished" (click)="toggleViewMode()">
<fa-icon *ngIf="!tableView" [icon]="faToggleOff" size="lg"></fa-icon>
<fa-icon *ngIf="tableView" [icon]="faToggleOn" size="lg"></fa-icon>
</label>
<label class="py-0 my-0">Table</label>
</div>
<app-confirm-button btnClass="btn btn-outline-danger my-2" (confirm)="delete(g)">
<fa-icon [icon]="faTrash"></fa-icon>
<span>Delete</span>
</app-confirm-button>
</div>
</div>

<ng-container *ngIf="games$ | async as games; else loading">

<!-- Card view mode -->
<ng-container *ngIf="!tableView">
<div class="dropzone col-3 card bg-transparent">
<app-dropzone class="h-100 my-3" (dropped)="dropped($event)">
<pre class="overflow-hidden">Drag in a yaml game array or</pre>
<button class="btn btn-secondary btn-sm" (click)="create()">
<fa-icon [icon]="faPlus"></fa-icon>
<span>New Game</span>
</button>
</app-dropzone>
</div>

<div class="wrapper col-3 my-3" *ngFor="let g of games; trackBy:trackById" (mouseenter)="on(g)"
(mouseleave)="off(g)" (focus)="on(g)" tabindex="0">

<app-game-card [game]="g"></app-game-card>

<div class="overlay d-flex flex-column justify-content-center align-items-center" *ngIf="hot===g">
<a class="btn btn-outline-info my-2" [routerLink]="['../registrar', g.id]">
<fa-icon [icon]="faUsers"></fa-icon>
<span>Players</span>
</a>
<a class="btn btn-outline-info my-2" [routerLink]="['../observer/challenges', g.id]">
<fa-icon [icon]="faTv"></fa-icon>
<span>Observe</span>
</a>
<a class="btn btn-outline-info my-2" [routerLink]="['../designer', g.id]">
<fa-icon [icon]="faCog"></fa-icon>
<span>Settings</span>
</a>
<div>
<button class="btn btn-outline-info btn-sm mx-1" (click)="clone(g)">
<fa-icon [icon]="faClone"></fa-icon>
<span>Clone</span>
</button>
<button class="btn btn-outline-info btn-sm mx-1" (click)="yaml(g)">
<fa-icon [icon]="faCopy"></fa-icon>
<span>yaml</span>
</button>
<button class="btn btn-outline-info btn-sm mx-1" (click)="json(g)">
<fa-icon [icon]="faCopy"></fa-icon>
<span>json</span>
</button>
</div>
<app-confirm-button btnClass="btn btn-outline-danger my-2" (confirm)="delete(g)">
<fa-icon [icon]="faTrash"></fa-icon>
<span>Delete</span>
</app-confirm-button>
</div>
</div>
</ng-container> <!-- End card view -->

<!-- Table view mode -->
<ng-container *ngIf="tableView">
<div class="col-12">
<div class="table-wrapper">
<table class="table mt-2 text-left">
<tbody>
<tr class="bg-dark">
<td class="thin-col sticky-card sticky-header"></td>
<td class="thin-col"></td>
<td class="thin-col"></td>
<td class="thin-col sticky-options sticky-header"></td>
<td class="sticky-name sticky-header">Name</td>
<td>Key</td>
<td>Series</td>
<td>Track</td>
<td>Season</td>
<td>Division</td>
<td>Mode</td>
<td class="date-col">Start Ex.</td>
<td class="date-col">End Ex.</td>
<td class="date-col">Start Reg.</td>
<td class="date-col">End Reg.</td>
<td class="text-right">Team Size</td>
<td class="text-right">Sess. Duration</td>
<td class="text-right">Sess. Limit</td>
<td class="text-right">Space Limit</td>
<td class="text-right">Max Attempts</td>
</tr>
<tr *ngFor="let game of games; trackBy:trackById">
<td class="thin-col sticky-card sticky-cell"><img [src]="game.cardUrl" class="table-card"></td>
<td class="thin-col">
<div [tooltip]="game.isPublished ? 'Published' : 'Unpublished'" containerClass="light-tooltip" container="body" [adaptivePosition]="true" placement="top">
<fa-icon [icon]="game.isPublished ? faGlobe : faEyeSlash"></fa-icon>
</div>
<div [tooltip]="game.allowReset ? 'Reset Allowed' : 'Reset Not Allowed'" containerClass="light-tooltip" container="body" [adaptivePosition]="true" placement="top">
<fa-icon [icon]="game.allowReset ? faUndo : faLock"></fa-icon>
</div>
</td>
<td class="thin-col">
<div [tooltip]="game.allowTeam ? 'Team' : 'Individual'" containerClass="light-tooltip" container="body" [adaptivePosition]="true" placement="top">
<fa-icon [icon]="game.allowTeam ? faTeam : faUser"></fa-icon>
</div>
<div [tooltip]="!!game.feedbackTemplate?.challenge?.length || !!game.feedbackTemplate?.game?.length ? 'Accepts Feedback' : 'No Feedback'" containerClass="light-tooltip" container="body" [adaptivePosition]="true" placement="top">
<fa-icon [icon]="!!game.feedbackTemplate?.challenge?.length || !!game.feedbackTemplate?.game?.length ? faChartBar : faCommentSlash"></fa-icon>
</div>
</td>
<td class="thin-col sticky-options sticky-cell">
<div dropdown class="btn-group" container="body" #dropdown="bs-dropdown" [autoClose]="true" [insideClick]="true">
<a class="btn btn-outline-info py-1 px-2" [routerLink]="['../designer', game.id]">
<fa-icon [icon]="faCog" size="sm"></fa-icon>
</a>
<button dropdownToggle class="btn btn-outline-info py-1 px-2 dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button>
<div *dropdownMenu class="dropdown-menu">
<a class="dropdown-item px-3" [routerLink]="['../registrar', game.id]"><fa-icon class="mr-1" [icon]="faUsers"></fa-icon>Players</a>
<a class="dropdown-item px-3" [routerLink]="['../observer/challenges', game.id]"><fa-icon class="mr-1" [icon]="faTv"></fa-icon>Observe</a>
<a class="dropdown-item px-3" [routerLink]="['/game', game.id]"><fa-icon class="mr-1" [icon]="faGamepad"></fa-icon>Lobby</a>
<a class="dropdown-item px-3" [routerLink]="['../designer', game.id]"><fa-icon class="mr-1" [icon]="faCog"></fa-icon>Settings</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item px-3" (click)="clone(game)"><fa-icon class="mr-1" [icon]="faClone"></fa-icon>Clone</a>
<a class="dropdown-item px-3" (click)="yaml(game); dropdown.isOpen = false;"><fa-icon class="mr-1" [icon]="faCopy"></fa-icon>YAML</a>
<a class="dropdown-item px-3" (click)="json(game); dropdown.isOpen = false;"><fa-icon class="mr-1" [icon]="faCopy"></fa-icon>JSON</a>
<div class="dropdown-divider"></div>
<app-confirm-button btnClass="dropdown-item px-3" (confirm)="delete(game)">
<fa-icon [icon]="faTrash"></fa-icon>
<span>Delete</span>
</app-confirm-button>
</div>
</div>
</td>
<td class="sticky-name sticky-cell name-col">
<div class="pr-1 d-flex flex-column">
<span>{{game.name}}</span>
<small class="text-muted">{{game.id | slice:0:12}}</small>
</div>
</td>
<td>{{game.key}}</td>
<td>{{game.competition}}</td>
<td>{{game.track}}</td>
<td>{{game.season}}</td>
<td>{{game.division}}</td>
<td>{{game.mode}}</td>
<td class="date-col">{{game.gameStart | shorttime}}</td>
<td class="date-col">{{game.gameEnd | shorttime}}</td>
<td class="date-col">{{game.registrationOpen | shorttime}}</td>
<td class="date-col">{{game.registrationClose | shorttime}}</td>
<td class="text-right">{{game.minTeamSize}}-{{game.maxTeamSize}}</td>
<td class="text-right">{{game.sessionMinutes}}m</td>
<td class="text-right">{{game.sessionLimit}}</td>
<td class="text-right">{{game.gamespaceLimitPerSession}}</td>
<td class="text-right">{{game.maxAttempts}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-container> <!-- End table view -->

</ng-container>

<!-- </div> -->
<div class="my-4"></div>

Expand All @@ -83,3 +210,9 @@ <h4>Announcement</h4>
<app-announce></app-announce>
</div>
</div>

<ng-template #loading>
<div class="text-center w-100">
<app-spinner></app-spinner>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,13 +1,97 @@
@import '../../../scss/variables';

.wrapper {
// position: relative;
}
.overlay {
position: absolute;
top: 1rem;
bottom: 1rem;
left: 1rem;
right: 1rem;
top: 0rem;
bottom: 0rem;
left: 0rem;
right: 0rem;
border-radius: .25rem;
background-color: rgba(black, .8);
z-index: 10;
}
.dropzone {
border-radius: 0;
border: 0;
}

.table-wrapper {
overflow-x: auto;
overflow-y: none;
background-color: $body-bg;
}
table {
border-collapse: separate;
border-spacing: 0;
}

td {
vertical-align: middle;
}
td:not(.thin-col) {
min-width: 80px;
max-width: 300px;
padding: 0.5rem;
word-wrap: break-word;
}
td.thin-col {
padding-left: 5px;
padding-right: 5px;
text-align: center;
}
td.name-col {
min-width: 250px;
max-width: 350px;
}

td.date-col {
min-width: 120px;
}

// 3 'sticky' columns to never scroll out of view
.sticky-card {
position: sticky;
left: 0;
}
.sticky-options {
position: sticky;
left: 45px;
z-index: 30;
}
.sticky-name {
position: sticky;
left: 112px;
div {
border-right: #444 solid 1px;
}
}
.sticky-cell {
background-color: $body-bg;
}
.sticky-header {
background-color: $dark;
}

.table-card {
height: 50px;
padding: 0;
margin: 0;
}

.dropdown-item {
cursor: pointer;
}

// for table scroll bar to match theme
.table-wrapper::-webkit-scrollbar {
background: $black;
}
.table-wrapper::-webkit-scrollbar-thumb {
background: $dark;
}
.table-wrapper::-webkit-scrollbar-thumb:hover {
background: darken($dark, 5%);
}
Loading

0 comments on commit f60d708

Please sign in to comment.