generated from bisco/codex-bootstrap
189 lines
6.4 KiB
HTML
189 lines
6.4 KiB
HTML
<main class="shell">
|
|
<header class="topbar">
|
|
<div>
|
|
<p class="eyebrow">Private basketball scouting</p>
|
|
<h1>HoopScout</h1>
|
|
</div>
|
|
<div class="scoreboard" aria-label="Scouting summary">
|
|
<div>
|
|
<span>{{ resultCount }}</span>
|
|
<small>matches</small>
|
|
</div>
|
|
<div>
|
|
<span>{{ averagePoints }}</span>
|
|
<small>avg PPG</small>
|
|
</div>
|
|
<div>
|
|
<span>{{ averageEfficiency }}</span>
|
|
<small>avg EFF</small>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="filters" aria-label="Player filters">
|
|
<label class="search-field">
|
|
Search
|
|
<input
|
|
type="search"
|
|
[(ngModel)]="filters.q"
|
|
(ngModelChange)="onFilterChange()"
|
|
placeholder="Name, role, team, nationality"
|
|
>
|
|
</label>
|
|
<label>
|
|
Position
|
|
<select [(ngModel)]="filters.position" (ngModelChange)="onFilterChange()">
|
|
<option value="">Any</option>
|
|
<option *ngFor="let position of positions" [value]="position">{{ position }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Role
|
|
<select [(ngModel)]="filters.role" (ngModelChange)="onFilterChange()">
|
|
<option value="">Any</option>
|
|
<option *ngFor="let role of roles" [value]="role">{{ role }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
League
|
|
<select [(ngModel)]="filters.league" (ngModelChange)="onFilterChange()">
|
|
<option value="">Any</option>
|
|
<option *ngFor="let league of leagues" [value]="league">{{ league }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Min PPG
|
|
<input type="number" [(ngModel)]="filters.minPoints" (ngModelChange)="onFilterChange()" min="0" step="0.1">
|
|
</label>
|
|
<label>
|
|
Min APG
|
|
<input type="number" [(ngModel)]="filters.minAssists" (ngModelChange)="onFilterChange()" min="0" step="0.1">
|
|
</label>
|
|
<label>
|
|
Min RPG
|
|
<input type="number" [(ngModel)]="filters.minRebounds" (ngModelChange)="onFilterChange()" min="0" step="0.1">
|
|
</label>
|
|
<label>
|
|
Min EFF
|
|
<input
|
|
type="number"
|
|
[(ngModel)]="filters.minEfficiency"
|
|
(ngModelChange)="onFilterChange()"
|
|
min="0"
|
|
step="0.1"
|
|
>
|
|
</label>
|
|
<div class="actions">
|
|
<button type="button" class="primary" (click)="search()">Refresh</button>
|
|
<button type="button" class="secondary" (click)="clearFilters()">Reset</button>
|
|
</div>
|
|
</section>
|
|
|
|
<p *ngIf="errorMessage" class="alert">{{ errorMessage }}</p>
|
|
|
|
<section class="workspace" aria-label="Scouting workspace">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Player</th>
|
|
<th>Pos</th>
|
|
<th>League</th>
|
|
<th>Team</th>
|
|
<th>
|
|
<button type="button" class="sort" [class.active]="activeSort === 'points_per_game'" (click)="sortBy('points_per_game')">PPG</button>
|
|
</th>
|
|
<th>
|
|
<button type="button" class="sort" [class.active]="activeSort === 'assists_per_game'" (click)="sortBy('assists_per_game')">APG</button>
|
|
</th>
|
|
<th>
|
|
<button type="button" class="sort" [class.active]="activeSort === 'rebounds_per_game'" (click)="sortBy('rebounds_per_game')">RPG</button>
|
|
</th>
|
|
<th>
|
|
<button type="button" class="sort" [class.active]="activeSort === 'efficiency_rating'" (click)="sortBy('efficiency_rating')">EFF</button>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
*ngFor="let player of players"
|
|
[class.selected]="selectedPlayer?.id === player.id"
|
|
(click)="selectPlayer(player)"
|
|
>
|
|
<td>
|
|
<strong>{{ player.name }}</strong>
|
|
<small>{{ player.nationality || '-' }} · {{ player.height_cm || '-' }} cm · {{ player.weight_kg || '-' }} kg</small>
|
|
</td>
|
|
<td>{{ player.position }}</td>
|
|
<td>{{ player.league?.code || '-' }}</td>
|
|
<td>{{ player.team?.name || '-' }}</td>
|
|
<td>{{ statValue(player, 'points_per_game') }}</td>
|
|
<td>{{ statValue(player, 'assists_per_game') }}</td>
|
|
<td>{{ statValue(player, 'rebounds_per_game') }}</td>
|
|
<td>{{ statValue(player, 'efficiency_rating') }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div *ngIf="!loading && players.length === 0" class="empty">No players match the current filters.</div>
|
|
<div *ngIf="showLoadingPlaceholder" class="empty">Loading scouting board...</div>
|
|
</div>
|
|
|
|
<aside class="detail" *ngIf="selectedPlayer">
|
|
<div>
|
|
<p class="eyebrow">Selected profile</p>
|
|
<h2>{{ selectedPlayer.name }}</h2>
|
|
<p class="role">{{ selectedPlayer.position }} · {{ selectedPlayer.role || 'Role pending' }}</p>
|
|
</div>
|
|
<div class="profile-strip">
|
|
<span>{{ selectedPlayer.nationality || '-' }}</span>
|
|
<span>{{ selectedPlayer.height_cm || '-' }} cm</span>
|
|
<span>{{ selectedPlayer.weight_kg || '-' }} kg</span>
|
|
</div>
|
|
<dl class="bio-grid">
|
|
<div>
|
|
<dt>League</dt>
|
|
<dd>{{ selectedPlayer.league?.name || '-' }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Team</dt>
|
|
<dd>{{ selectedPlayer.team?.name || '-' }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Born</dt>
|
|
<dd>{{ selectedPlayer.birth_year || '-' }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Size</dt>
|
|
<dd>{{ selectedPlayer.height_cm || '-' }} cm / {{ selectedPlayer.weight_kg || '-' }} kg</dd>
|
|
</div>
|
|
</dl>
|
|
<div class="metric-grid">
|
|
<div>
|
|
<span>PPG</span>
|
|
<strong>{{ statValue(selectedPlayer, 'points_per_game') }}</strong>
|
|
</div>
|
|
<div>
|
|
<span>APG</span>
|
|
<strong>{{ statValue(selectedPlayer, 'assists_per_game') }}</strong>
|
|
</div>
|
|
<div>
|
|
<span>RPG</span>
|
|
<strong>{{ statValue(selectedPlayer, 'rebounds_per_game') }}</strong>
|
|
</div>
|
|
<div>
|
|
<span>TS%</span>
|
|
<strong>{{ statValue(selectedPlayer, 'true_shooting_percentage') }}</strong>
|
|
</div>
|
|
<div>
|
|
<span>USG%</span>
|
|
<strong>{{ statValue(selectedPlayer, 'usage_percentage') }}</strong>
|
|
</div>
|
|
<div>
|
|
<span>Games</span>
|
|
<strong>{{ statValue(selectedPlayer, 'games_played') }}</strong>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</section>
|
|
</main>
|