Browse Source

- Added a cross route search feature

pull/1/head
Dan 1 year ago
parent
commit
b7eeb38c6f
  1. 148
      src/app/app.component.ts
  2. 7
      src/app/core/models/plan.model.ts
  3. 25
      src/app/core/services/search.service.ts
  4. 2
      src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts
  5. 83
      src/app/features/plan/planV2.component.ts
  6. 106
      src/app/features/pokemon/pokemon-carousel/pokemon-carousel.component.ts
  7. 19
      src/app/features/pokemon/pokemon-cell/pokemon-cell.component.ts
  8. 10
      src/app/features/pokemon/pokemon-grid/pokemon-grid.component.ts

148
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: `
<mat-toolbar color="primary" class="top-bar">
<div class="toolbar-left">
<span>OriginDex</span>
<div class="search-container">
<mat-form-field appearance="fill" class="search-field">
<mat-label>Search Pokémon</mat-label>
<input type="text"
matInput
[formControl]="searchControl"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete"
(optionSelected)="onSearchSelect($event)"
[displayWith]="displayFn">
<mat-option *ngFor="let result of filteredOptions | async" [value]="result">
{{ result.pokemon }}
<span *ngIf="result.boxNumber !== undefined">
(Box {{ result.boxNumber + 1 }})
</span>
<span *ngIf="result.game_id !== undefined">
({{ result.game_id }})
</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
<div class="image-container">
@ -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<PokemonSearchResult[]>;
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<string, PokemonSearchResult>();
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('');
}
}

7
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
}

25
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<any>(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;
}

2
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) => {

83
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
></app-plan-game>
</div>
</div>
@ -45,13 +48,14 @@ import { AuthService } from '../../core/services/auth.service';
<div class="middle-section">
<div class="pokemon-section" *ngIf="selectedGame">
<cdk-virtual-scroll-viewport [itemSize]="56" class="pokemon-viewport">
<cdk-virtual-scroll-viewport [itemSize]="56" class="pokemon-viewport" #viewport>
<div *cdkVirtualFor="let pokemon of selectedGame.pokemon | keyvalue; trackBy: trackByPfic">
<app-plan-pokemonV2
[pokemon_family]="pokemon.value"
(statusUpdate)="onPokemonStatusUpdate($event)"
[isSelected]="selectedPokemon?.family_pfic === pokemon.key"
(familySelected)="selectPokemon($event)"
#familyComponent
></app-plan-pokemonV2>
</div>
</cdk-virtual-scroll-viewport>
@ -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<ElementRef>;
@ViewChildren('familyComponent', { read: ElementRef }) familyElements!: QueryList<ElementRef>;
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');
}
}
}

106
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: `
<div class="search-container">
<mat-form-field appearance="fill" class="search-field">
<mat-label>Search Pokémon</mat-label>
<input type="text"
matInput
[formControl]="searchControl"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete"
(optionSelected)="onSearchSelect($event)"
[displayWith]="displayFn">
<mat-option *ngFor="let result of filteredOptions | async" [value]="result">
{{ result.pokemon.Name }}
<span *ngIf="result.pokemon.Form">
({{ result.pokemon.Form }})
</span>
<span *ngIf="result.boxNumber !== undefined">
(Box {{ result.boxNumber + 1 }})
</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<div class="carousel-container">
<button mat-icon-button class="nav-button prev"
[disabled]="currentBoxIndex === 0"
@ -98,15 +70,6 @@ interface PokemonSearchResult {
></app-pokemon-details>
`,
styles: [`
.search-container {
display: flex;
justify-content: center;
}
.search-field {
width: 100%;
max-width: 400px;
}
.carousel-container {
display: flex;
align-items: center;
@ -127,7 +90,7 @@ interface PokemonSearchResult {
background-color: white;
border: 2px solid #ccc;
border-radius: 10px;
padding: 20px;
padding: 10px;
position: relative;
width: 100%;
box-sizing: border-box;
@ -173,8 +136,8 @@ export class PokemonCarouselComponent implements OnInit {
currentBoxIndex = 0;
selectedPokemon: Pokemon | null = null;
caughtPokemon = new Set<string>();
searchControl = new FormControl('');
filteredOptions: Observable<PokemonSearchResult[]>;
private subscription: Subscription | null = null;
get currentGroup(): (Pokemon | null)[] {
return this.pokemonGroups[this.currentBoxIndex] || [];
@ -182,16 +145,28 @@ export class PokemonCarouselComponent implements OnInit {
constructor(
private pokemonService: PokemonService,
private cdr: ChangeDetectorRef
) {
this.filteredOptions = this.searchControl.valueChanges.pipe(
startWith(''),
map(value => this.filterPokemon(value))
);
}
private cdr: ChangeDetectorRef,
private searchService: SearchService
) {}
ngOnInit() {
this.loadPokemon();
this.subscription = this.searchService.selectedItem$.subscribe((item) => {
const result: PokemonSearchResult = item as PokemonSearchResult;
if(!result){
return
}
if(result.boxNumber){
this.currentBoxIndex = result.boxNumber;
this.cdr.markForCheck();
}
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
private loadPokemon() {
@ -234,37 +209,4 @@ export class PokemonCarouselComponent implements OnInit {
this.selectedPokemon = pokemon;
this.cdr.markForCheck();
}
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: PokemonSearchResult[] = [];
this.pokemonGroups.forEach((group, boxIndex) => {
group.forEach(pokemon => {
if (pokemon && pokemon.Name.toLowerCase().includes(searchTerm)) {
results.push({
pokemon,
boxNumber: boxIndex
});
}
});
});
return results.slice(0, 10); // Limit to 10 results
}
displayFn(result: PokemonSearchResult): string {
return result?.pokemon?.Name || '';
}
onSearchSelect(event: any) {
const result: PokemonSearchResult = event.option.value;
this.currentBoxIndex = result.boxNumber;
this.searchControl.setValue('');
this.cdr.markForCheck();
}
}

19
src/app/features/pokemon/pokemon-cell/pokemon-cell.component.ts

@ -58,14 +58,14 @@ import { PokemonService } from '../../../core/services/pokemon.service';
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 10px;
padding: 10px;
padding: 5px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
//justify-content: space-between;
cursor: pointer;
height: 180px;
height: 130px;
}
.pokemon-cell.empty {
@ -76,25 +76,26 @@ import { PokemonService } from '../../../core/services/pokemon.service';
.pokemon-name {
font-weight: bold;
font-size: 0.8em;
margin-bottom: 5px;
height: 2.4em; /* Allows for 2 lines of text */
//margin-bottom: 5px;
height: 1em; /* Allows for 2 lines of text */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.pokemon-image {
width: 96px;
height: 96px;
width: 70px;
height: 70px;
object-fit: contain;
background-color: #f9f9f9; /* Light gray background for placeholder */
}
.pokemon-form {
min-height: 2.4em;
line-height: 1.2em;
//line-height: 1.2em;
font-style: italic;
font-size: 0.7em;
margin-top: 5px;
//margin-top: 5px;
margin-bottom: 2px;
}
.pokemon-info {
display: flex;

10
src/app/features/pokemon/pokemon-grid/pokemon-grid.component.ts

@ -1,7 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { PokemonService } from '../../../core/services/pokemon.service';
import { Pokemon } from '../../../core/models/pokemon.model';
import { PokemonCellComponent } from '../pokemon-cell/pokemon-cell.component';
@ -14,13 +13,11 @@ import { PokemonDetailsComponent } from '../pokemon-details/pokemon-details.comp
imports: [
CommonModule,
MatCardModule,
ScrollingModule,
PokemonCellComponent,
PokemonDetailsComponent
],
template: `
<div class="pokemon-boxes">
<cdk-virtual-scroll-viewport itemSize="400" class="viewport">
<mat-card *ngFor="let group of pokemonGroups; let i = index" class="pokemon-box">
<div class="box-title">Box {{ (i + 1).toString().padStart(3, '0') }}</div>
<div class="pokemon-grid">
@ -32,7 +29,6 @@ import { PokemonDetailsComponent } from '../pokemon-details/pokemon-details.comp
></app-pokemon-cell>
</div>
</mat-card>
</cdk-virtual-scroll-viewport>
</div>
<app-pokemon-details
@ -46,7 +42,7 @@ import { PokemonDetailsComponent } from '../pokemon-details/pokemon-details.comp
.pokemon-boxes {
height: calc(100vh - 100px); /* Adjust based on your layout */
overflow: hidden;
padding: 20px;
padding: 10px;
transition: margin-right 0.3s ease-in-out;
display: flex;
flex-direction: column;
@ -63,7 +59,7 @@ import { PokemonDetailsComponent } from '../pokemon-details/pokemon-details.comp
border: 2px solid #ccc;
border-radius: 10px;
margin: 0 10px 30px;
padding: 20px;
padding: 10px;
position: relative;
width: calc(100% - 20px);
max-width: 800px;
@ -84,7 +80,7 @@ import { PokemonDetailsComponent } from '../pokemon-details/pokemon-details.comp
.pokemon-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
gap: 5px;
}
`]
})

Loading…
Cancel
Save