From b7eeb38c6f3f55db06026be3553a5f3eba6becbf Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 21 Nov 2024 12:03:39 +0000 Subject: [PATCH] - Added a cross route search feature --- src/app/app.component.ts | 148 +++++++++++++++++- src/app/core/models/plan.model.ts | 7 + src/app/core/services/search.service.ts | 25 +++ .../plan-pokemon-details.component.ts | 2 + src/app/features/plan/planV2.component.ts | 83 +++++++++- .../pokemon-carousel.component.ts | 106 +++---------- .../pokemon-cell/pokemon-cell.component.ts | 19 +-- .../pokemon-grid/pokemon-grid.component.ts | 10 +- 8 files changed, 295 insertions(+), 105 deletions(-) create mode 100644 src/app/core/services/search.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0b5e43b..e387686 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,10 +4,20 @@ import { Router, RouterOutlet, RouterLink, RouterLinkActive } from '@angular/rou import { MatToolbarModule } from '@angular/material/toolbar'; import { MatButtonModule } from '@angular/material/button'; import { MatTabsModule } from '@angular/material/tabs'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { AuthService } from './core/services/auth.service'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatListModule } from '@angular/material/list'; import { PokemonService } from './core/services/pokemon.service'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { map, startWith } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { Pokemon } from './core/models/pokemon.model'; +import { PokemonSearchResult, SearchService } from './core/services/search.service'; +import { PlanService } from './core/services/plan.service'; +import { GamePlan } from './core/models/plan.model'; @Component({ selector: 'app-root', @@ -21,12 +31,39 @@ import { PokemonService } from './core/services/pokemon.service'; MatButtonModule, MatTabsModule, MatSidenavModule, - MatListModule + MatListModule, + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + ReactiveFormsModule ], template: `
OriginDex + +
+ + Search Pokémon + + + + {{ result.pokemon }} + + (Box {{ result.boxNumber + 1 }}) + + + ({{ result.game_id }}) + + + + +
@@ -104,7 +141,18 @@ import { PokemonService } from './core/services/pokemon.service'; transform: scale(1.1); /* Slightly enlarge the image on hover */ } + .search-container { + display: flex; + justify-content: center; + margin-left:20px; + margin-top: 30px; + } + .search-field { + width: 100%; + max-width: 400px; + min-width: 300px; + } mat-toolbar { margin-bottom: 0; @@ -141,21 +189,115 @@ import { PokemonService } from './core/services/pokemon.service'; }) export class AppComponent { hoveredRoute: string = ''; + + searchControl = new FormControl(''); + filteredOptions: Observable; + + pokemonGroups: (Pokemon | null)[][] = []; + gamePlans: GamePlan[] = []; + constructor( public auth: AuthService, public pokemonService: PokemonService, + private planService: PlanService, private authService: AuthService, - private router: Router + private router: Router, + private searchService: SearchService ) { this.authService.isAuthenticated$.subscribe((isAuthenticated) => { if (isAuthenticated) { this.pokemonService.initializeCaughtPokemon(); - console.log("Loading Plan") } }); + + this.filteredOptions = this.searchControl.valueChanges.pipe( + startWith(''), + map(value => this.filterPokemon(value)) + ); + + this.pokemonService.getPokemonBoxList().subscribe({ + next: (groups) => { + this.pokemonGroups = groups; + }, + error: (error) => { + console.error('Error loading Pokemon:', error); + } + }); + + this.planService.getPlan().subscribe( + plan => { + this.gamePlans = plan; + } + ); } isRouteSelected(route: string): boolean { return this.router.url === route || this.hoveredRoute === route; } + + private filterPokemon(value: string | PokemonSearchResult | null): PokemonSearchResult[] { + if (!value) return []; + + const searchTerm = typeof value === 'string' ? value.toLowerCase() : ''; + if (searchTerm.length < 2) return []; // Only search with 2 or more characters + + const results_map = new Map(); + + if(this.isRouteSelected("/storage-carousel")){ + this.pokemonGroups.forEach((group, boxIndex) => { + group.forEach(pokemon => { + if (pokemon && pokemon.Name.toLowerCase().includes(searchTerm)) { + const uniqueKey = `${pokemon.Name}-${boxIndex}`; + results_map.set(uniqueKey, { + pokemon: pokemon.Name, + boxNumber: boxIndex, + }); + } + }); + }); + } + else if (this.isRouteSelected("/efficiency")) { + this.gamePlans.forEach((gamePlan, planIndex) => { + for (let family in gamePlan.pokemon) { + if(!gamePlan.pokemon[family].evolve_to_augmented) { + continue + } + for (let pkmn of gamePlan.pokemon[family].evolve_to_augmented) { + if (pkmn.name.toLowerCase().includes(searchTerm)) { + const uniqueKey = `${pkmn.name}-${gamePlan.game_name}`; + results_map.set(uniqueKey, { + pokemon: pkmn.name, + game_id: gamePlan.game_name, + }); + } + } + if(!gamePlan.pokemon[family].breed_for_augmented) { + continue + } + for (let pkmn of gamePlan.pokemon[family].breed_for_augmented) { + if (pkmn.name.toLowerCase().includes(searchTerm)) { + const uniqueKey = `${pkmn.name}-${gamePlan.game_name}`; + results_map.set(uniqueKey, { + pokemon: pkmn.name, + game_id: gamePlan.game_name, + }); + } + } + } + }); + } + + const results: PokemonSearchResult[] = Array.from(results_map.values()); + return results.slice(0, 10); // Limit to 10 results + } + + displayFn(result: PokemonSearchResult): string { + return result?.pokemon || ''; + } + + onSearchSelect(event: any) { + const result: PokemonSearchResult = event.option.value; + this.searchService.setSelectedItem(result); + this.searchControl.setValue(''); + } } \ No newline at end of file diff --git a/src/app/core/models/plan.model.ts b/src/app/core/models/plan.model.ts index c9e1bce..4c2802f 100644 --- a/src/app/core/models/plan.model.ts +++ b/src/app/core/models/plan.model.ts @@ -12,4 +12,11 @@ export interface PokemonFamilyEntry { Any?: number; Male?: number; Female?: number; + evolve_to_augmented?: PokemonEntry[] + breed_for_augmented?: PokemonEntry[] +} + +interface PokemonEntry { + pfic: string + name: string } \ No newline at end of file diff --git a/src/app/core/services/search.service.ts b/src/app/core/services/search.service.ts new file mode 100644 index 0000000..99d5dc4 --- /dev/null +++ b/src/app/core/services/search.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Pokemon } from '../models/pokemon.model'; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + // This subject will hold the selected item value from the search bar + private selectedItemSubject = new BehaviorSubject(null); + + // Observable to subscribe to when the selected item changes + selectedItem$ = this.selectedItemSubject.asObservable(); + + // Method to update the selected item + setSelectedItem(item: any) { + this.selectedItemSubject.next(item); + } +} + +export interface PokemonSearchResult { + pokemon: string; + boxNumber?: number; + game_id?: string; +} diff --git a/src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts b/src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts index 052d465..972e5a7 100644 --- a/src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts +++ b/src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts @@ -156,6 +156,7 @@ export class PlanPokemonDetailsComponent { loadPokemonFamilyInfo(newFamily: PokemonFamilyEntry) { const evolveToArray: Pokemon[] = []; + this.evolve_to = [] newFamily.evolve_to.forEach((target) => { this.pokemonService.getPokemonFromPFIC(target).subscribe({ next: (pokemon) => { @@ -174,6 +175,7 @@ export class PlanPokemonDetailsComponent { }); const breedForArray: Pokemon[] = []; + this.breed_for = [] newFamily.breed_for.forEach((target) => { this.pokemonService.getPokemonFromPFIC(target).subscribe({ next: (pokemon) => { diff --git a/src/app/features/plan/planV2.component.ts b/src/app/features/plan/planV2.component.ts index 2827b8e..3a77d2b 100644 --- a/src/app/features/plan/planV2.component.ts +++ b/src/app/features/plan/planV2.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { FormsModule } from '@angular/forms'; @@ -9,6 +9,8 @@ import { PlanPokemonV2Component } from "./plan-pokemon/plan-pokemonV2.component" import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { PlanPokemonDetailsComponent } from "./plan-pokemon-details/plan-pokemon-details.component"; import { AuthService } from '../../core/services/auth.service'; +import { Subscription } from 'rxjs'; +import { PokemonSearchResult, SearchService } from '../../core/services/search.service'; @Component({ selector: 'app-planV2', @@ -32,6 +34,7 @@ import { AuthService } from '../../core/services/auth.service'; [game]="game" [isSelected]="selectedGame?.game_name === game.game_name" (gameSelect)="selectGame($event)" + #gameComponent >
@@ -45,13 +48,14 @@ import { AuthService } from '../../core/services/auth.service';
- +
@@ -148,10 +152,16 @@ export class PlanV2Component implements OnInit { selectedGame: GamePlan | null = null; selectedPokemon: PokemonFamilyEntry | null = null; + private subscription: Subscription | null = null; + + @ViewChildren('gameComponent', { read: ElementRef }) gameElements!: QueryList; + @ViewChildren('familyComponent', { read: ElementRef }) familyElements!: QueryList; + constructor( private planService: PlanService, private cdr: ChangeDetectorRef, - private authService: AuthService + private authService: AuthService, + private searchService: SearchService ) {} ngOnInit() { @@ -161,6 +171,45 @@ export class PlanV2Component implements OnInit { console.log("Loading Plan") } }); + this.subscription = this.searchService.selectedItem$.subscribe((item) => { + const result: PokemonSearchResult = item as PokemonSearchResult; + if(!result){ + return + } + if(result.game_id){ + for(const plan of this.gamePlans) { + if(plan.game_name === result.game_id) { + this.selectedGame = plan + for (let family in this.selectedGame.pokemon) { + const family_pkmn = this.selectedGame.pokemon[family] + if(family_pkmn.evolve_to_augmented) { + const foundObject = family_pkmn.evolve_to_augmented.find(obj => obj.name === result.pokemon); + if (foundObject) { + this.selectPokemon(family_pkmn) + break; + } + } + if(family_pkmn.breed_for_augmented) { + const foundObject = family_pkmn.breed_for_augmented.find(obj => obj.name === result.pokemon); + if (foundObject) { + this.selectPokemon(family_pkmn) + break; + } + } + } + this.scrollToSelected(); + break; + } + } + this.cdr.markForCheck(); + } + }); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } } private loadPlan() { @@ -176,7 +225,7 @@ export class PlanV2Component implements OnInit { selectGame(game: GamePlan) { if (this.viewport) { - this.viewport.scrollToIndex(0); // Reset scroll to top when switching games + //this.viewport.scrollToIndex(0); // Reset scroll to top when switching games } this.selectedGame = null; // Clear the selected game first to avoid stale data this.cdr.detectChanges(); @@ -195,5 +244,31 @@ export class PlanV2Component implements OnInit { trackByPfic(index: number, item: any): string { return item.key; // Assuming PFIC or another unique identifier is available } + + scrollToSelected(): void { + if(!this.selectedGame){ + return; + } + + const selectedIndex = this.gamePlans.indexOf(this.selectedGame); + if (selectedIndex >= 0) { + const selectedElement = this.gameElements.toArray()[selectedIndex]; + selectedElement.nativeElement.scrollIntoView({ + behavior: 'smooth', // Adds smooth scrolling + block: 'center', // Centers the item in the view + }); + } + + let selectedIndex_pkmn = -1; + for (const key in this.selectedGame.pokemon) { + selectedIndex_pkmn += 1; + if (this.selectedGame.pokemon[key] === this.selectedPokemon) { + break; + } + } + if (selectedIndex_pkmn >= 0) { + this.viewport.scrollToIndex(selectedIndex_pkmn, 'smooth'); + } + } } diff --git a/src/app/features/pokemon/pokemon-carousel/pokemon-carousel.component.ts b/src/app/features/pokemon/pokemon-carousel/pokemon-carousel.component.ts index 2bc9137..def66ed 100644 --- a/src/app/features/pokemon/pokemon-carousel/pokemon-carousel.component.ts +++ b/src/app/features/pokemon/pokemon-carousel/pokemon-carousel.component.ts @@ -11,13 +11,8 @@ import { PokemonService } from '../../../core/services/pokemon.service'; import { Pokemon } from '../../../core/models/pokemon.model'; import { PokemonCellComponent } from '../pokemon-cell/pokemon-cell.component'; import { PokemonDetailsComponent } from '../pokemon-details/pokemon-details.component'; -import { map, startWith } from 'rxjs/operators'; -import { Observable } from 'rxjs'; - -interface PokemonSearchResult { - pokemon: Pokemon; - boxNumber: number; -} +import { PokemonSearchResult, SearchService } from '../../../core/services/search.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-pokemon-carousel', @@ -35,29 +30,6 @@ interface PokemonSearchResult { PokemonDetailsComponent ], template: ` -
- - Search Pokémon - - - - {{ result.pokemon.Name }} - - ({{ result.pokemon.Form }}) - - - (Box {{ result.boxNumber + 1 }}) - - - - -
-