Skip to content

Commit

Permalink
feat: player search (#13)
Browse files Browse the repository at this point in the history
* adding search to dashboard

* add search to players get and count
  • Loading branch information
codephobia authored Mar 9, 2024
1 parent 91bcd66 commit eb586cd
Show file tree
Hide file tree
Showing 12 changed files with 42,294 additions and 42,121 deletions.
15 changes: 14 additions & 1 deletion apps/dashboard/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { NgModule } from '@angular/core';
import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { StoreModule } from '@ngrx/store';
import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
Expand All @@ -23,6 +26,16 @@ const COMPONENTS = [
BrowserAnimationsModule,
FontAwesomeModule,
AppRoutingModule,
StoreModule.forRoot({
router: routerReducer,
}),
StoreRouterConnectingModule.forRoot(),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: !isDevMode(),
autoPause: true,
trace: false,
}),
],
providers: [{
provide: ENV_CONFIG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class PlayerModalStore extends ComponentStore<PlayerModalState> {

public readonly getPlayers = this.effect<number>(page$ => page$.pipe(
switchMap(page => forkJoin([
this.playersService.find(page),
this.playersService.find({ page }),
this.playersService.count(),
]).pipe(
tapResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ <h1 class="text-white uppercase text-4xl px-10">Players</h1>
<a class="text-black bg-white bg-opacity-30 hover:bg-opacity-90 uppercase rounded py-2.5 px-5 text-xs whitespace-nowrap" routerLink="./create">Create Player</a>
<div class="flex flex-col w-full items-end px-10">
<div class="flex flex-row flex-grow w-full justify-end gap-10">
<dashboard-search class="flex flex-col flex-grow w-full max-w-sm"></dashboard-search>
<dashboard-search class="flex flex-col flex-grow w-full max-w-sm" [search]="vm.search" (onSearch)="onSearch($event)"></dashboard-search>
<dashboard-pagination class="flex flex-col" [page]="vm.page" [count]="vm.count" [perPage]="perPage" (pageChange)="onPageChange($event)"></dashboard-pagination>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';

import { PageEvent } from '@dashboard/components/pagination';
import { PlayersListStore } from './players-list.store';
import { SearchEvent } from '@dashboard/components/search';

@Component({
selector: 'pool-overlay-players-list-page',
Expand All @@ -19,13 +20,24 @@ export class PlayersListPageComponent {
) { }

public onPageChange({ page }: PageEvent): void {
const newPage = page > 1 ? page : undefined;

this.router.navigate(['.'], {
relativeTo: this.activatedRoute,
queryParams: { page },
queryParams: { page: newPage },
queryParamsHandling: 'merge',
});
}

public onSearch({ search }: SearchEvent): void {
const newSearch = search?.length ? search : undefined;

this.router.navigate(['.'], {
relativeTo: this.activatedRoute,
queryParams: { search: newSearch },
});
}

public deletePlayerById({ playerId }: { playerId: number }): void {
this.playersListStore.deletePlayerById(playerId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,45 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Store, createSelector } from '@ngrx/store';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { forkJoin } from 'rxjs';
import { combineLatest, forkJoin } from 'rxjs';
import { concatMap, map, switchMap, takeUntil } from 'rxjs/operators';

import { IPlayer } from '@pool-overlay/models';
import { PlayersService } from '../../../shared/services/players.service';
import { PlayersService, PlayerFindOptions } from '../../../shared/services/players.service';
import { selectQueryParam } from '../../../shared/utils/router.selectors';

export interface PlayersListState {
loaded: boolean;
page: number;
count: number;
players: IPlayer[];
}

@Injectable()
export class PlayersListStore extends ComponentStore<PlayersListState> {
constructor(
route: ActivatedRoute,
private playersService: PlayersService,
private store: Store,
) {
super({
loaded: false,
page: 1,
count: 0,
players: []
});

route.queryParamMap.pipe(
map(params => Number(params.get('page'))),
combineLatest([
this.store.select(this.selectPage),
this.store.select(this.selectSearch),
]).pipe(
takeUntil(this.destroy$),
).subscribe(page => {
const newPage = page ? page : 1;
this.setPage(newPage);
this.getPlayers(newPage);
});
).subscribe(([page, search]) => this.getPlayers({ page, search }));
}

public readonly setLoaded = this.updater<boolean>((state, loaded) => ({
...state,
loaded,
}));

public readonly setPage = this.updater<number>((state, page) => ({
...state,
page,
}));

public readonly setCount = this.updater<number>((state, count) => ({
...state,
count,
Expand All @@ -69,27 +62,37 @@ export class PlayersListStore extends ComponentStore<PlayersListState> {
};
});

private readonly selectPage = createSelector(
selectQueryParam('page'),
(page) => Number(page) > 0 ? Number(page) : 1
);
private readonly selectSearch = createSelector(
selectQueryParam('search'),
search => search ? String(search) : ''
);

public readonly loaded$ = this.select(state => state.loaded);
public readonly page$ = this.select(state => state.page);
public readonly count$ = this.select(state => state.count);
public readonly players$ = this.select(state => state.players);
public readonly vm$ = this.select(
this.loaded$,
this.page$,
this.store.select(this.selectPage),
this.store.select(this.selectSearch),
this.count$,
this.players$,
(loaded, page, count, players) => ({
(loaded, page, search, count, players) => ({
loaded,
page,
search,
count,
players,
})
);

public readonly getPlayers = this.effect<number>(page$ => page$.pipe(
switchMap(page => forkJoin([
this.playersService.find(page),
this.playersService.count(),
public readonly getPlayers = this.effect<PlayerFindOptions>(options$ => options$.pipe(
switchMap(({ page, search }) => forkJoin([
this.playersService.find({ page, search }),
this.playersService.count({ search }),
]).pipe(
tapResponse(
([players, { count }]) => {
Expand Down
27 changes: 23 additions & 4 deletions apps/dashboard/src/app/shared/services/players.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import { EnvironmentConfig, ENV_CONFIG } from '../../models/environment-config.m
import { IPlayer } from '@pool-overlay/models';
import { ICount } from '../../models/count.model';

export interface PlayerFindOptions {
page: number;
search?: string;
}

export interface PlayerCountOptions {
search?: string;
}

@Injectable()
export class PlayersService {
private apiURL: string;
Expand All @@ -20,8 +29,13 @@ export class PlayersService {
this.apiVersion = config.environment.apiVersion;
}

public find(page = 1): Observable<IPlayer[]> {
const url = `${this.apiURL}/${this.apiVersion}/${this.endpoint}?page=${page}`;
public find({ page, search }: PlayerFindOptions = { page: 1 }): Observable<IPlayer[]> {
let url = `${this.apiURL}/${this.apiVersion}/${this.endpoint}?page=${page}`;

if (search) {
url = url + `&search=${search}`;
}

return this.http.get<IPlayer[]>(url);
}

Expand All @@ -30,8 +44,13 @@ export class PlayersService {
return this.http.get<IPlayer>(url);
}

public count(): Observable<ICount> {
const url = `${this.apiURL}/${this.apiVersion}/${this.endpoint}/count`;
public count({ search }: PlayerCountOptions = {}): Observable<ICount> {
let url = `${this.apiURL}/${this.apiVersion}/${this.endpoint}/count`;

if (search) {
url = url + `?search=${search}`;
}

return this.http.get<ICount>(url);
}

Expand Down
13 changes: 13 additions & 0 deletions apps/dashboard/src/app/shared/utils/router.selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getSelectors } from '@ngrx/router-store';

export const {
selectCurrentRoute, // select the current route
selectFragment, // select the current route fragment
selectQueryParams, // select the current route query params
selectQueryParam, // factory function to select a query param
selectRouteParams, // select the current route params
selectRouteParam, // factory function to select a route param
selectRouteData, // select the current route data
selectUrl, // select the current url
selectTitle, // select the title if available
} = getSelectors();
1 change: 1 addition & 0 deletions libs/dashboard/components/search/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SearchModule } from './lib/search.module';
export { SearchEvent } from './lib/search.component';
14 changes: 8 additions & 6 deletions libs/dashboard/components/search/src/lib/search.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { faMagnifyingGlass } from '@fortawesome/pro-regular-svg-icons';

let idCounter = 0;

export interface SearchEvent {
search: string;
}

@Component({
selector: 'dashboard-search',
templateUrl: './search.component.html'
Expand All @@ -20,7 +24,7 @@ export class SearchComponent {
}

@Output()
public onSearch = new EventEmitter<{ search: string }>();
public onSearch = new EventEmitter<SearchEvent>();

public faMagnifyingGlass = faMagnifyingGlass;
public form: FormGroup;
Expand All @@ -31,13 +35,11 @@ export class SearchComponent {
this.id = `dashboard-search-${++idCounter}`;

this.form = this._fb.group({
search: '',
search: new FormControl(''),
});
}

public submit(): void {
this.onSearch.emit({
search: '',
});
this.onSearch.emit(this.form.value);
}
}
35 changes: 31 additions & 4 deletions libs/go/api/players.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func (server *Server) handlePlayers() http.Handler {

// Players handler for GET method. Returns a page of players.
func (server *Server) handlePlayersGet(w http.ResponseWriter, r *http.Request) {
// where clause for find and count
where := make(map[string]interface{})

// get query vars
v := r.URL.Query()

Expand All @@ -70,17 +73,25 @@ func (server *Server) handlePlayersGet(w http.ResponseWriter, r *http.Request) {
return
}

// get search
search := v.Get("search")

// only add search to where if there is a length
if len(search) > 0 {
where["name"] = search
}

// get count of players to test page ceiling
var count int64
countResult := server.db.Model(&models.Player{}).Count(&count)
countResult := server.db.Model(&models.Player{}).Where(where).Count(&count)
if countResult.Error != nil {
server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
return
}

// check if page is beyond maximum
totalPages := int(math.Ceil(float64(count) / playersPerPage))
if pageNum > totalPages {
if pageNum > totalPages && totalPages != 0 {
server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidPageNumber)
return
}
Expand All @@ -91,6 +102,7 @@ func (server *Server) handlePlayersGet(w http.ResponseWriter, r *http.Request) {
offset := pageNum*playersPerPage - playersPerPage
playersResult := server.db.
Select("id", "name", "flag_id", "fargo_observable_id", "fargo_id", "fargo_rating").
Where(where).
Order("name").
Limit(playersPerPage).
Offset(offset).
Expand Down Expand Up @@ -152,9 +164,24 @@ func (server *Server) handlePlayersCount() http.Handler {

// Players count handler for GET method.
func (server *Server) handlePlayersCountGet(w http.ResponseWriter, r *http.Request) {
// get count of players
var count int64
countResult := server.db.Model(&models.Player{}).Count(&count)

// where clause for count
where := make(map[string]interface{})

// get query vars
v := r.URL.Query()

// get search
search := v.Get("search")

// only add search to where if there is a length
if len(search) > 0 {
where["name"] = search
}

// get count of players
countResult := server.db.Model(&models.Player{}).Where(where).Count(&count)
if countResult.Error != nil {
server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
return
Expand Down
Loading

0 comments on commit eb586cd

Please sign in to comment.