generated from bisco/codex-bootstrap
Compare commits
10 Commits
f8244849a3
...
dd46d7aabb
| Author | SHA1 | Date | |
|---|---|---|---|
| dd46d7aabb | |||
| f8de0e644a | |||
| 2c7ec7383b | |||
| f87f62f111 | |||
| f44a07f231 | |||
| 8909be9694 | |||
| 03b8762835 | |||
| 5cc1076225 | |||
| f2934d7924 | |||
| 1b7b4259f4 |
@@ -21,7 +21,7 @@ The initial schema stores:
|
|||||||
- per-season averages, totals, and advanced metrics;
|
- per-season averages, totals, and advanced metrics;
|
||||||
- per-game logs for best and worst performance views.
|
- per-game logs for best and worst performance views.
|
||||||
|
|
||||||
The Angular dashboard applies live filter updates with a short debounce, quick position/league chips, local stat sorting, summary metrics, and a persistent profile panel on desktop.
|
The Angular dashboard applies filters only when the user refreshes the result set, then supports local stat sorting, summary metrics, and an open/close profile panel on desktop.
|
||||||
|
|
||||||
## External Integrations
|
## External Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -126,23 +126,12 @@ button {
|
|||||||
background: #e7eeeb;
|
background: #e7eeeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickbar {
|
.compact {
|
||||||
display: grid;
|
min-height: 34px;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
padding: 0 10px;
|
||||||
gap: 10px;
|
white-space: nowrap;
|
||||||
align-items: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip-group button,
|
|
||||||
.sort {
|
.sort {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
@@ -152,20 +141,12 @@ button {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip-group button.active,
|
|
||||||
.sort.active {
|
.sort.active {
|
||||||
color: white;
|
color: white;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-count {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 800;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
@@ -177,12 +158,16 @@ button {
|
|||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 390px;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-top: 8px;
|
margin-top: 14px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace.detail-open {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 390px;
|
||||||
|
}
|
||||||
|
|
||||||
.table-wrap,
|
.table-wrap,
|
||||||
.detail {
|
.detail {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@@ -254,6 +239,13 @@ td small {
|
|||||||
top: 14px;
|
top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.role {
|
.role {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -336,6 +328,10 @@ dd {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace.detail-open {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
@@ -351,8 +347,7 @@ dd {
|
|||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar,
|
.topbar {
|
||||||
.quickbar {
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -390,6 +385,11 @@ dd {
|
|||||||
h1 {
|
h1 {
|
||||||
font-size: 2.6rem;
|
font-size: 2.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
|
|||||||
@@ -26,51 +26,47 @@
|
|||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
[(ngModel)]="filters.q"
|
[(ngModel)]="filters.q"
|
||||||
(ngModelChange)="onFilterChange()"
|
|
||||||
placeholder="Name, role, team, nationality"
|
placeholder="Name, role, team, nationality"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Position
|
Position
|
||||||
<select [(ngModel)]="filters.position" (ngModelChange)="onFilterChange()">
|
<select [(ngModel)]="filters.position">
|
||||||
<option value="">Any</option>
|
<option value="">Any</option>
|
||||||
<option *ngFor="let position of positions" [value]="position">{{ position }}</option>
|
<option *ngFor="let position of positions" [value]="position">{{ position }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Role
|
Role
|
||||||
<input
|
<select [(ngModel)]="filters.role">
|
||||||
type="text"
|
<option value="">Any</option>
|
||||||
[(ngModel)]="filters.role"
|
<option *ngFor="let role of roles" [value]="role">{{ role }}</option>
|
||||||
(ngModelChange)="onFilterChange()"
|
</select>
|
||||||
placeholder="3 and D, handler, rim runner"
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
League
|
League
|
||||||
<select [(ngModel)]="filters.league" (ngModelChange)="onFilterChange()">
|
<select [(ngModel)]="filters.league">
|
||||||
<option value="">Any</option>
|
<option value="">Any</option>
|
||||||
<option *ngFor="let league of leagues" [value]="league">{{ league }}</option>
|
<option *ngFor="let league of leagues" [value]="league">{{ league }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Min PPG
|
Min PPG
|
||||||
<input type="number" [(ngModel)]="filters.minPoints" (ngModelChange)="onFilterChange()" min="0" step="0.1">
|
<input type="number" [(ngModel)]="filters.minPoints" min="0" step="0.1">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Min APG
|
Min APG
|
||||||
<input type="number" [(ngModel)]="filters.minAssists" (ngModelChange)="onFilterChange()" min="0" step="0.1">
|
<input type="number" [(ngModel)]="filters.minAssists" min="0" step="0.1">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Min RPG
|
Min RPG
|
||||||
<input type="number" [(ngModel)]="filters.minRebounds" (ngModelChange)="onFilterChange()" min="0" step="0.1">
|
<input type="number" [(ngModel)]="filters.minRebounds" min="0" step="0.1">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Min EFF
|
Min EFF
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="filters.minEfficiency"
|
[(ngModel)]="filters.minEfficiency"
|
||||||
(ngModelChange)="onFilterChange()"
|
|
||||||
min="0"
|
min="0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
>
|
>
|
||||||
@@ -81,33 +77,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="quickbar" aria-label="Quick filters">
|
|
||||||
<div class="chip-group">
|
|
||||||
<button
|
|
||||||
*ngFor="let position of positions"
|
|
||||||
type="button"
|
|
||||||
[class.active]="filters.position === position"
|
|
||||||
(click)="togglePosition(position)"
|
|
||||||
>
|
|
||||||
{{ position }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="chip-group league-chips">
|
|
||||||
<button
|
|
||||||
*ngFor="let league of leagues"
|
|
||||||
type="button"
|
|
||||||
[class.active]="filters.league === league"
|
|
||||||
(click)="toggleLeague(league)"
|
|
||||||
>
|
|
||||||
{{ league }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="filter-count">{{ activeFiltersCount }} active</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p *ngIf="errorMessage" class="alert">{{ errorMessage }}</p>
|
<p *ngIf="errorMessage" class="alert">{{ errorMessage }}</p>
|
||||||
|
|
||||||
<section class="workspace" aria-label="Scouting workspace">
|
<section class="workspace" [class.detail-open]="selectedPlayer" aria-label="Scouting workspace">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -151,14 +123,17 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div *ngIf="!loading && players.length === 0" class="empty">No players match the current filters.</div>
|
<div *ngIf="!loading && players.length === 0" class="empty">No players match the current filters.</div>
|
||||||
<div *ngIf="loading" class="empty">Loading scouting board...</div>
|
<div *ngIf="showLoadingPlaceholder" class="empty">Loading scouting board...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="detail" *ngIf="selectedPlayer">
|
<aside class="detail" *ngIf="selectedPlayer">
|
||||||
<div>
|
<div class="detail-header">
|
||||||
<p class="eyebrow">Selected profile</p>
|
<div>
|
||||||
<h2>{{ selectedPlayer.name }}</h2>
|
<p class="eyebrow">Selected profile</p>
|
||||||
<p class="role">{{ selectedPlayer.position }} · {{ selectedPlayer.role || 'Role pending' }}</p>
|
<h2>{{ selectedPlayer.name }}</h2>
|
||||||
|
<p class="role">{{ selectedPlayer.position }} · {{ selectedPlayer.role || 'Role pending' }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="secondary compact" (click)="returnToResults()">Back to results</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-strip">
|
<div class="profile-strip">
|
||||||
<span>{{ selectedPlayer.nationality || '-' }}</span>
|
<span>{{ selectedPlayer.nationality || '-' }}</span>
|
||||||
|
|||||||
@@ -71,22 +71,61 @@ describe('AppComponent', () => {
|
|||||||
assert.equal(component.resultCount, 2);
|
assert.equal(component.resultCount, 2);
|
||||||
assert.equal(component.averagePoints, '14.00');
|
assert.equal(component.averagePoints, '14.00');
|
||||||
assert.equal(component.topEfficiencyPlayer?.name, 'Luca Marini');
|
assert.equal(component.topEfficiencyPlayer?.name, 'Luca Marini');
|
||||||
|
assert.equal(component.selectedPlayer, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reacts to filter changes without requiring the search button', () => {
|
it('does not apply changed filters until refresh is requested', () => {
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
|
let requestedLeague = '';
|
||||||
const api = {
|
const api = {
|
||||||
searchPlayers: () => {
|
searchPlayers: (filters: { league: string }) => {
|
||||||
calls += 1;
|
calls += 1;
|
||||||
|
requestedLeague = filters.league;
|
||||||
return of({ count: 0, results: [] });
|
return of({ count: 0, results: [] });
|
||||||
},
|
},
|
||||||
} as unknown as PlayerApiService;
|
} as unknown as PlayerApiService;
|
||||||
const component = new AppComponent(api);
|
const component = new AppComponent(api);
|
||||||
|
|
||||||
component.onFilterChange();
|
component.filters.league = 'LBA';
|
||||||
component.flushPendingSearch();
|
component.filters.league = 'ABA';
|
||||||
|
|
||||||
|
assert.equal(calls, 0);
|
||||||
|
|
||||||
|
component.search();
|
||||||
|
|
||||||
assert.equal(calls, 1);
|
assert.equal(calls, 1);
|
||||||
|
assert.equal(requestedLeague, 'ABA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes role options and sends the selected role as a filter', () => {
|
||||||
|
let requestedRole = '';
|
||||||
|
const api = {
|
||||||
|
searchPlayers: (filters: { role: string }) => {
|
||||||
|
requestedRole = filters.role;
|
||||||
|
return of({ count: 0, results: [] });
|
||||||
|
},
|
||||||
|
} as unknown as PlayerApiService;
|
||||||
|
const component = new AppComponent(api);
|
||||||
|
|
||||||
|
assert.ok(component.roles.includes('3 and D wing'));
|
||||||
|
|
||||||
|
component.filters.role = '3 and D wing';
|
||||||
|
component.search();
|
||||||
|
|
||||||
|
assert.equal(requestedRole, '3 and D wing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the loading placeholder only before results exist', () => {
|
||||||
|
const api = {
|
||||||
|
searchPlayers: () => of({ count: samplePlayers.length, results: samplePlayers }),
|
||||||
|
} as unknown as PlayerApiService;
|
||||||
|
const component = new AppComponent(api);
|
||||||
|
|
||||||
|
component.loading = true;
|
||||||
|
assert.equal(component.showLoadingPlaceholder, true);
|
||||||
|
|
||||||
|
component.players = samplePlayers;
|
||||||
|
assert.equal(component.showLoadingPlaceholder, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sorts the visible scouting board by selected stat', () => {
|
it('sorts the visible scouting board by selected stat', () => {
|
||||||
@@ -100,4 +139,19 @@ describe('AppComponent', () => {
|
|||||||
|
|
||||||
assert.equal(component.players[0].name, 'Mateo Santos');
|
assert.equal(component.players[0].name, 'Mateo Santos');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens and closes a player profile without losing the filtered list', () => {
|
||||||
|
const api = {
|
||||||
|
searchPlayers: () => of({ count: samplePlayers.length, results: samplePlayers }),
|
||||||
|
} as unknown as PlayerApiService;
|
||||||
|
const component = new AppComponent(api);
|
||||||
|
|
||||||
|
component.search();
|
||||||
|
component.selectPlayer(samplePlayers[0]);
|
||||||
|
component.returnToResults();
|
||||||
|
|
||||||
|
assert.equal(component.selectedPlayer, null);
|
||||||
|
assert.equal(component.players.length, 2);
|
||||||
|
assert.equal(component.resultCount, 2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,34 @@ type SortKey = 'efficiency_rating' | 'points_per_game' | 'assists_per_game' | 'r
|
|||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
readonly positions = ['PG', 'SG', 'SF', 'PF', 'C'];
|
readonly positions = ['PG', 'SG', 'SF', 'PF', 'C'];
|
||||||
readonly leagues = ['LBA', 'ACB', 'ABA', 'BBL', 'BSL', 'LNB', 'ISBL', 'NBL', 'NZNBL'];
|
readonly leagues = ['LBA', 'ACB', 'ABA', 'BBL', 'BSL', 'LNB', 'ISBL', 'NBL', 'NZNBL'];
|
||||||
|
readonly roles = [
|
||||||
|
'3 and D wing',
|
||||||
|
'Change-of-pace guard',
|
||||||
|
'Connector wing',
|
||||||
|
'Defensive playmaker',
|
||||||
|
'Drive and kick guard',
|
||||||
|
'Face-up forward',
|
||||||
|
'Glass cleaner',
|
||||||
|
'Low-post finisher',
|
||||||
|
'Movement shooter',
|
||||||
|
'Off-screen scorer',
|
||||||
|
'Paint anchor',
|
||||||
|
'Paint touch guard',
|
||||||
|
'Pick and roll creator',
|
||||||
|
'Pressure guard',
|
||||||
|
'Primary ball handler',
|
||||||
|
'Pull-up shooter',
|
||||||
|
'Rim protector',
|
||||||
|
'Roll man',
|
||||||
|
'Secondary creator',
|
||||||
|
'Short-roll passer',
|
||||||
|
'Slashing wing',
|
||||||
|
'Stretch four',
|
||||||
|
'Switch defender',
|
||||||
|
'Tempo guard',
|
||||||
|
'Transition wing',
|
||||||
|
'Vertical spacer',
|
||||||
|
];
|
||||||
readonly sortOptions: Array<{ key: SortKey; label: string }> = [
|
readonly sortOptions: Array<{ key: SortKey; label: string }> = [
|
||||||
{ key: 'efficiency_rating', label: 'EFF' },
|
{ key: 'efficiency_rating', label: 'EFF' },
|
||||||
{ key: 'points_per_game', label: 'PPG' },
|
{ key: 'points_per_game', label: 'PPG' },
|
||||||
@@ -41,7 +69,6 @@ export class AppComponent {
|
|||||||
loading = false;
|
loading = false;
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
activeSort: SortKey = 'efficiency_rating';
|
activeSort: SortKey = 'efficiency_rating';
|
||||||
private pendingSearch: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
constructor(private readonly playerApi: PlayerApiService) {}
|
constructor(private readonly playerApi: PlayerApiService) {}
|
||||||
|
|
||||||
@@ -50,15 +77,15 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(): void {
|
search(): void {
|
||||||
this.cancelPendingSearch();
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
|
|
||||||
this.playerApi.searchPlayers(this.filters).subscribe({
|
this.playerApi.searchPlayers(this.filters).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.players = this.sortPlayers(response.results);
|
const players = this.sortPlayers(response.results);
|
||||||
|
this.players = players;
|
||||||
this.resultCount = response.count;
|
this.resultCount = response.count;
|
||||||
this.selectedPlayer = this.players[0] ?? null;
|
this.selectedPlayer = this.matchSelectedPlayer(players);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -68,19 +95,6 @@ export class AppComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterChange(): void {
|
|
||||||
this.cancelPendingSearch();
|
|
||||||
this.pendingSearch = setTimeout(() => this.search(), 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
flushPendingSearch(): void {
|
|
||||||
if (!this.pendingSearch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cancelPendingSearch();
|
|
||||||
this.search();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFilters(): void {
|
clearFilters(): void {
|
||||||
this.filters = {
|
this.filters = {
|
||||||
q: '',
|
q: '',
|
||||||
@@ -95,26 +109,20 @@ export class AppComponent {
|
|||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePosition(position: string): void {
|
|
||||||
this.filters.position = this.filters.position === position ? '' : position;
|
|
||||||
this.onFilterChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLeague(league: string): void {
|
|
||||||
this.filters.league = this.filters.league === league ? '' : league;
|
|
||||||
this.onFilterChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
sortBy(key: SortKey): void {
|
sortBy(key: SortKey): void {
|
||||||
this.activeSort = key;
|
this.activeSort = key;
|
||||||
this.players = this.sortPlayers(this.players);
|
this.players = this.sortPlayers(this.players);
|
||||||
this.selectedPlayer = this.players[0] ?? null;
|
this.selectedPlayer = this.matchSelectedPlayer(this.players);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectPlayer(player: PlayerSummary): void {
|
selectPlayer(player: PlayerSummary): void {
|
||||||
this.selectedPlayer = player;
|
this.selectedPlayer = player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
returnToResults(): void {
|
||||||
|
this.selectedPlayer = null;
|
||||||
|
}
|
||||||
|
|
||||||
statValue(player: PlayerSummary, key: keyof NonNullable<PlayerSummary['stats']>): string {
|
statValue(player: PlayerSummary, key: keyof NonNullable<PlayerSummary['stats']>): string {
|
||||||
const value = player.stats?.[key];
|
const value = player.stats?.[key];
|
||||||
return value === undefined || value === null ? '-' : String(value);
|
return value === undefined || value === null ? '-' : String(value);
|
||||||
@@ -140,17 +148,8 @@ export class AppComponent {
|
|||||||
return (total / this.players.length).toFixed(2);
|
return (total / this.players.length).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeFiltersCount(): number {
|
get showLoadingPlaceholder(): boolean {
|
||||||
return [
|
return this.loading && this.players.length === 0;
|
||||||
this.filters.q,
|
|
||||||
this.filters.position,
|
|
||||||
this.filters.role,
|
|
||||||
this.filters.league,
|
|
||||||
this.filters.minPoints,
|
|
||||||
this.filters.minAssists,
|
|
||||||
this.filters.minRebounds,
|
|
||||||
this.filters.minEfficiency,
|
|
||||||
].filter((value) => value !== null && String(value).trim().length > 0).length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortPlayers(players: PlayerSummary[]): PlayerSummary[] {
|
private sortPlayers(players: PlayerSummary[]): PlayerSummary[] {
|
||||||
@@ -161,10 +160,11 @@ export class AppComponent {
|
|||||||
return Number(player.stats?.[key] ?? 0);
|
return Number(player.stats?.[key] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelPendingSearch(): void {
|
private matchSelectedPlayer(players: PlayerSummary[]): PlayerSummary | null {
|
||||||
if (this.pendingSearch) {
|
if (!this.selectedPlayer) {
|
||||||
clearTimeout(this.pendingSearch);
|
return null;
|
||||||
this.pendingSearch = null;
|
|
||||||
}
|
}
|
||||||
|
return players.find((player) => player.id === this.selectedPlayer?.id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user