diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6cb4c05 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}/", + "runtimeExecutable": "${env:APPDATA}\\..\\Local\\Vivaldi\\Application\\vivaldi.exe", + "sourceMaps": true, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "--user-data-dir=${workspaceFolder}/DevProfile" + ], + "sourceMapPathOverrides": { + "webpack:///./src/*": "${webRoot}/src/*", + "webpack:///src/*": "${webRoot}/src/*", + "webpack:///*": "*", + "/./*": "${webRoot}/*", + "/src/*": "${webRoot}/src/*", + "/*": "*", + "/./~/*": "${webRoot}/node_modules/*" + }, + "port": 9222, + "trace": true, + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 12e671c..2a139ec 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "ng": "ng", "start": "ng serve", "build": "ng build", + "build_prod": "ng build --configuration production", "watch": "ng build --watch --configuration development", "test": "ng test", "serve:ssr:origin-dex": "node dist/origin-dex/server/server.mjs" diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3c8cd69..e387686 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,13 +1,23 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router'; +import { Router, RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router'; 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,43 +31,127 @@ import { PokemonService } from './core/services/pokemon.service'; MatButtonModule, MatTabsModule, MatSidenavModule, - MatListModule + MatListModule, + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + ReactiveFormsModule ], template: ` - - OriginDex - - + +
+ OriginDex + +
+ + Search Pokémon + + + + {{ result.pokemon }} + + (Box {{ result.boxNumber + 1 }}) + + + ({{ result.game_id }}) + + + + +
+
+ +
+ + +
+ +
Welcome, {{ auth.currentUser?.username }}! - +
+ - - +
+ + +
- - - - - Storage Carousel - - - Efficiency Plan - - - - -
- -
-
-
+ +
+ +
`, styles: [` - .spacer { - flex: 1 1 auto; + .top-bar { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + + .toolbar-left { + display: flex; + align-items: center; + flex: 1; + } + + .image-container { + display: flex; + align-items: center; + gap: 16px; /* Space between images */ + flex: 0; /* Do not grow or shrink */ + } + + .toolbar-right { + display: flex; + align-items: center; + gap: 8px; /* Space between the welcome message and buttons */ + flex: 1; + justify-content: flex-end; + } + + .top-bar-icon { + cursor: pointer; + height: 48px; /* Adjust as needed */ + width: auto; /* Maintain aspect ratio */ + transition: transform 0.3s ease, opacity 0.3s ease; /* Smooth transition effect */ + } + + .top-bar-icon:hover { + 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 { @@ -75,7 +169,7 @@ import { PokemonService } from './core/services/pokemon.service'; } .content { - height: 100%; + //height: 100%; overflow: auto; } @@ -94,10 +188,116 @@ 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 + public pokemonService: PokemonService, + private planService: PlanService, + private authService: AuthService, + private router: Router, + private searchService: SearchService ) { - this.pokemonService.initializeCaughtPokemon(); + this.authService.isAuthenticated$.subscribe((isAuthenticated) => { + if (isAuthenticated) { + this.pokemonService.initializeCaughtPokemon(); + } + }); + + 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/app.routes.ts b/src/app/app.routes.ts index 98bebd3..80ffbe1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -19,8 +19,8 @@ export const routes: Routes = [ }, { path: 'efficiency', - loadComponent: () => import('./features/plan/plan.component') - .then(m => m.PlanComponent), + loadComponent: () => import('./features/plan/planV2.component') + .then(m => m.PlanV2Component), canActivate: [AuthGuard] }, { diff --git a/src/app/core/models/plan.model.ts b/src/app/core/models/plan.model.ts index 8a6b683..b51bfd6 100644 --- a/src/app/core/models/plan.model.ts +++ b/src/app/core/models/plan.model.ts @@ -1,29 +1,23 @@ export interface GamePlan { - game_name: string; - game_id: number; - pokemon: PlanPokemon[]; - } - - export interface PlanPokemon { - pfic: string; - name: string; - form_name?: string; - catch_count: number; - evolve_to: EvolutionTarget[]; - breed_for: BreedingTarget[]; - } - - export interface EvolutionTarget { - pfic: string; - name: string; - form_name?: string; - method: string; - count: number; - } - - export interface BreedingTarget { - pfic: string; - name: string; - form_name?: string; - count: number; - } \ No newline at end of file + game_name: string; + pokemon: Record; +} + +export interface PokemonFamilyEntry { + family_pfic?: string; + representative: string; + catch_count: number; + caught_count: number; + evolve_to: string[]; + breed_for: string[]; + 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/models/pokemon.model.ts b/src/app/core/models/pokemon.model.ts index 6c4d75c..e2ed7a4 100644 --- a/src/app/core/models/pokemon.model.ts +++ b/src/app/core/models/pokemon.model.ts @@ -3,15 +3,17 @@ export interface Pokemon { Name: string; Form: string | null; NationalDex: number; - Generation?: number; + Generation: number; StorableInHome?: boolean; IsBabyForm?: boolean; Encounters?: PokemonEncounter[]; MarkIcon?: string; - MarkName?: string; + MarkName: string; Image?: string; IsDefault?: boolean; IsCaught?: boolean; + IsGenderRelevant?: boolean; + EvolutionMethod?: string; } export interface PokemonEncounter { diff --git a/src/app/core/services/plan.service.ts b/src/app/core/services/plan.service.ts index 7b37f48..447f5d8 100644 --- a/src/app/core/services/plan.service.ts +++ b/src/app/core/services/plan.service.ts @@ -1,56 +1,69 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, Subject, take } from 'rxjs'; +import { map, Observable, pipe, shareReplay, Subject, take, tap } from 'rxjs'; import { environment } from '../../../environments/environment.development'; -import { GamePlan } from '../models/plan.model'; +import { GamePlan, PokemonFamilyEntry } from '../models/plan.model'; +import { PokemonService } from './pokemon.service'; @Injectable({ providedIn: 'root' }) export class PlanService { - private caughtPokemon = new Set(); private gameUpdates = new Subject<{gameId: number, total: number}>(); gameUpdates$ = this.gameUpdates.asObservable(); - constructor(private http: HttpClient) {} + private gamePlanCache: Observable<(GamePlan[])> | null = null; + private gamePlan: GamePlan[] = []; + + constructor( + private http: HttpClient, + private pokemonService: PokemonService + ) {} getPlan(): Observable { - return this.http.get(`${environment.apiUrl}/plan`); + if (this.gamePlanCache) { + return this.gamePlanCache; + } + this.gamePlanCache = this.http.get(`${environment.apiUrl}/plan`).pipe( + tap(game_plan => { + this.gamePlan = game_plan as GamePlan[]; + }), + shareReplay(1) + ); + return this.gamePlanCache; } - updateCaughtStatus(pfic: string, caught: boolean) { - if (caught) { - this.caughtPokemon.add(pfic); - } else { - this.caughtPokemon.delete(pfic); + private calculateGameTotal(game: GamePlan): number { + var sum = 0; + for(const family in game.pokemon) + { + sum += game.pokemon[family].catch_count; } - // Trigger recalculation of affected games - this.recalculateAffectedGames(pfic); + return sum } - private recalculateAffectedGames(pfic: string) { - // This would need to check all games for the affected Pokemon - // and update their totals accordingly - this.getPlan().pipe(take(1)).subscribe(games => { - games.forEach(game => { - const affectedPokemon = game.pokemon.find(p => - p.pfic === pfic || - p.evolve_to.some(e => e.pfic === pfic) || - p.breed_for.some(b => b.pfic === pfic) - ); - if (affectedPokemon) { - this.gameUpdates.next({ - gameId: game.game_id, - total: this.calculateGameTotal(game) - }); + updateCaughtCount(family: PokemonFamilyEntry) { + for(const plan of this.gamePlan) { + if(family.family_pfic && family.family_pfic in plan.pokemon) { + let pokemon_family = plan.pokemon[family.family_pfic] + let count = 0; + + for( const pfic of pokemon_family.evolve_to) { + if (this.pokemonService.isTargetCompleted(pfic)) { + count += 1; + } } - }); - }); - } - private calculateGameTotal(game: GamePlan): number { - return game.pokemon.reduce((total, pokemon) => total + pokemon.catch_count, 0); + for( const pfic of pokemon_family.breed_for) { + if (this.pokemonService.isTargetCompleted(pfic)) { + count += 1; + } + } + + pokemon_family.caught_count = count; + } + } } } \ No newline at end of file diff --git a/src/app/core/services/pokemon.service.ts b/src/app/core/services/pokemon.service.ts index 8c7aa36..795d48e 100644 --- a/src/app/core/services/pokemon.service.ts +++ b/src/app/core/services/pokemon.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { map, Observable, shareReplay, tap } from 'rxjs'; +import { catchError, concatMap, map, Observable, of, pipe, shareReplay, tap } from 'rxjs'; import { Pokemon, PokemonEncounter } from '../models/pokemon.model'; import { environment } from '../../../environments/environment.development'; import { comparePfics } from '../utils/pfic-utils'; @@ -12,41 +12,124 @@ export class PokemonService { private apiUrl = environment.apiUrl; private pokemonCache: Observable<(Pokemon | null)[][]> | null = null; private pokemonGroups: (Pokemon | null)[][] = []; + private pokemonFormMap: Map = new Map(); + + private pokemonFormCache: Observable<(Pokemon)[]> | null = null; constructor(private http: HttpClient) { } - getPokemonList(): Observable<(Pokemon | null)[][]> { + getPokemonBoxList(): Observable<(Pokemon | null)[][]> { if (this.pokemonCache) { return this.pokemonCache; } - this.pokemonCache = this.http.get<(any | null)[][]>(`${this.apiUrl}/pokemon`).pipe( - map(groups => groups.map(group => - group.map(pokemon => pokemon ? { - PFIC: pokemon.PFIC, - Name: pokemon.name, - Form: pokemon.form_name, - NationalDex: pokemon.national_dex, - Generation: pokemon.generation, - StorableInHome: pokemon.storable_in_home, - IsBabyForm: pokemon.is_baby_form, - Encounters: pokemon.encounters || [], - MarkIcon: pokemon.icon_path, - MarkName: pokemon.mark_name, - Image: this.getPokemonImageUrl(pokemon.PFIC), - IsDefault: pokemon.is_default || false, - IsCaught: this.caughtPokemon.has(pokemon.PFIC) - } as Pokemon : null) - )), - tap(groups => { - this.pokemonGroups = groups; // Store the groups for later updates + this.pokemonCache = this.getPokemonList().pipe( + map((pokemonList) => { + const boxes: (Pokemon | null)[][] = []; + let currentBox: (Pokemon | null)[] = []; + let currentGeneration = 0; + let currentDexNumber = 0; + let formsGroup: Pokemon[] = []; + + for (const pokemon of pokemonList) { + // Start a new NationalDex group if needed + if (pokemon.NationalDex !== currentDexNumber) { + // If formsGroup has Pokémon, add them to the current box + if (formsGroup.length > 0) { + for (const form of formsGroup) { + currentBox.push(form); + if (currentBox.length === 30) { + boxes.push([...currentBox]); + currentBox = []; + } + } + formsGroup = []; + } + currentDexNumber = pokemon.NationalDex; + + // Start a new generation box if needed + if (currentGeneration !== pokemon.Generation) { + if (currentBox.length > 0) { + while (currentBox.length < 30) { + currentBox.push(null); + } + boxes.push([...currentBox]); + currentBox = []; + } + currentGeneration = pokemon.Generation; + } + } + + // Add the Pokémon form to the group for the current NationalDex + formsGroup.push(pokemon); + } + + // Add any remaining forms in the last group + for (const form of formsGroup) { + currentBox.push(form); + if (currentBox.length === 30) { + boxes.push([...currentBox]); + currentBox = []; + } + } + + // Add the last box with padding if needed + if (currentBox.length > 0) { + while (currentBox.length < 30) { + currentBox.push(null); + } + boxes.push(currentBox); + } + + return boxes; }), + // Share the result to cache it for future subscribers shareReplay(1) - ); + ); return this.pokemonCache; } + getPokemonList(){ + if(this.pokemonFormCache) { + return this.pokemonFormCache; + } + this.pokemonFormCache = this.http.get(`${this.apiUrl}/pokemon`).pipe( + map((rows) => rows.map(row => { + const pkmn = { + PFIC: row.pfic, + Name: row.data.name, + Form: row.data.form_name, + NationalDex: row.data.national_dex, + Generation: row.data.generation, + StorableInHome: row.data.storable_in_home, + IsBabyForm: row.data.is_baby_form, + Encounters: row.data.encounters || [], + MarkIcon: "", + MarkName: row.data.mark, + Image: "", + IsDefault: row.data.is_default || false, + IsGenderRelevant: row.data.gender_relevant, + IsCaught: this.caughtPokemon.has(row.pfic), + EvolutionMethod: row.data.evolution_method || "" + } as Pokemon; + pkmn.MarkIcon = this.getMarkImgName(pkmn.MarkName) + pkmn.Image = this.getPokemonImageUrl(pkmn) + return pkmn; + }) + ), + tap(pokemon_list => { + pokemon_list.forEach(mon => { + this.pokemonFormMap.set(mon.PFIC, mon); + }); + }), + // Cache the result for future subscribers + shareReplay(1) + ); + + return this.pokemonFormCache; + } + getPokemonDetails(pfic: string): Observable { return this.http.get(`${this.apiUrl}/pokemon/${pfic}`); } @@ -65,13 +148,12 @@ export class PokemonService { } private updatePokemonCaughtStatus(pfic: string, isCaught: boolean) { - this.pokemonGroups.forEach(group => { - group.forEach(pokemon => { - if (pokemon && pokemon.PFIC === pfic) { - pokemon.IsCaught = isCaught; - } - }); - }); + if(this.pokemonFormMap.has(pfic)) { + const pkmn = this.pokemonFormMap.get(pfic) + if (pkmn) { + pkmn.IsCaught = isCaught; + } + } } getCaughtPokemon(): Observable { @@ -84,6 +166,58 @@ export class PokemonService { return this.caughtPokemon.has(pfic); } + getPokemonFromPFIC(pfic: string): Observable { + // Check the cache for the PFIC + const cachedPokemon = this.pokemonFormMap.get(pfic); + if (cachedPokemon) { + // Return the cached Pokémon as an Observable + return new Observable((observer) => { + observer.next(cachedPokemon); + observer.complete(); + }); + } + + // If not in cache, fetch from the server + return this.http.get(`${this.apiUrl}/pokemon/${pfic}/details`).pipe( + map(data => { + const pkmn = { + PFIC: data.pfic, + Name: data.data.name, + Form: data.data.form_name, + NationalDex: data.data.national_dex, + Generation: data.data.generation, + StorableInHome: data.data.storable_in_home, + IsBabyForm: data.data.is_baby_form, + Encounters: data.data.encounters || [], + MarkIcon: "", + MarkName: data.data.mark, + Image: "", + IsDefault: data.data.is_default || false, + IsGenderRelevant: data.data.gender_relevant, + IsCaught: this.caughtPokemon.has(data.pfic), + EvolutionMethod: data.data.evolution_method || "" + } as Pokemon; + pkmn.MarkIcon = this.getMarkImgName(pkmn.MarkName) + pkmn.Image = this.getPokemonImageUrl(pkmn) + return pkmn; + }), + tap((pokemon) => { + // Cache the result for future requests + this.pokemonFormMap.set(pfic, pokemon); + }), + map((pokemon) => pokemon), // Map the server response directly to the output + shareReplay(1), // Cache the HTTP result for future subscribers + catchError((error): Observable => { + console.error(`Error fetching Pokémon with PFIC ${pfic}:`, error); + return new Observable((observer) => { + observer.next(null); + observer.complete(); + }); + }) + ); + } + + updateCaughtStatus(pfic: string, caught: boolean) { if (caught) { this.caughtPokemon.add(pfic); @@ -92,8 +226,40 @@ export class PokemonService { } } - getPokemonImageUrl(pfic: string): string { - return `/assets/images/pokemon/${pfic}.png`; + getPokemonImageUrl(data: Pokemon | null): string { + if (data === null) { + return ""; + } + if (data.IsGenderRelevant) { + return `/assets/images/pokemon/${data.PFIC}.png`; + } + const gender_less = data.PFIC.slice(0, -1) + '0' + return `/assets/images/pokemon/${gender_less}.png`; + } + + getMarkImgName(mark: string): string { + switch (mark) { + case "Game Boy": + return "images/marks/GB_icon_HOME.png" + case "Markless": + return "images/marks/Markless_icon_HOME.png" + case "Alola": + return "images/marks/Black_clover_HOME.png" + case "Kalos": + return "images/marks/Blue_pentagon_HOME.png" + case "Let's Go": + return "images/marks/Let's_Go_icon_HOME.png" + case "Galar": + return "images/marks/Galar_symbol_HOME.png" + case "Sinnoh": + return "images/marks/BDSP_icon_HOME.png" + case "Hisui": + return "images/marks/Arceus_mark_HOME.png" + case "Paldea": + return "images/marks/Paldea_icon_HOME.png" + } + + return "images/marks/Markless_icon_HOME.png" } getMarkImageUrl(markName: string): string { @@ -104,10 +270,7 @@ export class PokemonService { this.getCaughtPokemon().subscribe( pfics => { pfics.forEach(pfic => this.updateCaughtStatus(pfic, true)); - // If pokemon are already loaded, update their status - if (this.pokemonGroups.length > 0) { - pfics.forEach(pfic => this.updatePokemonCaughtStatus(pfic, true)); - } + pfics.forEach(pfic => this.updatePokemonCaughtStatus(pfic, true)); } ); } 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-game/plan-game.component.ts b/src/app/features/plan/plan-game/plan-game.component.ts index 1b84538..7fdf84c 100644 --- a/src/app/features/plan/plan-game/plan-game.component.ts +++ b/src/app/features/plan/plan-game/plan-game.component.ts @@ -20,12 +20,6 @@ import { GamePlan } from '../../../core/models/plan.model'; [alt]="game.game_name" class="game-image" > - -

{{ game.game_name }}

-

- Pokémon to catch: {{ getTotalCatchCount() }} -

-
`, styles: [` @@ -47,7 +41,6 @@ import { GamePlan } from '../../../core/models/plan.model'; .game-image { width: 100%; - height: 160px; object-fit: cover; } @@ -73,12 +66,24 @@ export class PlanGameComponent { @Output() gameSelect = new EventEmitter(); getTotalCatchCount(): number { - return this.game.pokemon.reduce((sum, pokemon) => sum + pokemon.catch_count, 0); + var sum = 0; + for(const family in this.game.pokemon) + { + sum += this.game.pokemon[family].catch_count; + } + return sum } getGameBoxArt(): string { - // You'll need to implement this to return the correct box art URL - return `/assets/images/games/${this.game.game_name.toLowerCase().replace(' ', '-')}.png`; + switch(this.game.game_name){ + case "Legends: Arceus": { + return `/assets/images/games/_LegendsArceus.png`; + } + default: { + return `/assets/images/games/_${this.game.game_name.replace(' ', '')}.png`; + } + } + } onSelect() { 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 new file mode 100644 index 0000000..2305af2 --- /dev/null +++ b/src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts @@ -0,0 +1,266 @@ +import { Component, Input, Output, EventEmitter, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PokemonFamilyEntry } from '../../../core/models/plan.model'; +import { Pokemon } from '../../../core/models/pokemon.model'; +import { PokemonService } from '../../../core/services/pokemon.service'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatTabsModule } from '@angular/material/tabs'; +import { PlanService } from '../../../core/services/plan.service'; + +export interface PokemonCaughtStatusUpdate { + pokemon: Pokemon; + familyEntry: PokemonFamilyEntry; +} + +@Component({ +selector: 'app-plan-pokemon-details', +standalone: true, +imports: [ + CommonModule, + MatChipsModule, + MatTabsModule +], +template: ` + + + +
+
+ +
+ +
+
+ + {{ target.Name }} + ({{ target.Form }}) + + {{target?.EvolutionMethod}} +
+
+
+
+ + + +
+
+ +
+ + {{ target.Name }} + ({{ target.Form }}) + + + Breed + +
+
+
+
+
+`, +styles: [` +.scrollable-content { + max-height: calc(100vh - 400px); /* Adjust as necessary for the available space */ + overflow-y: auto; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, auto)); + gap: 16px; + padding: 16px 8px; /* Adjust padding to your liking */ +} + +.target-card { + display: flex; + //flex-direction: column; /* Stack image and details vertically */ + //align-items: flex-start; + gap: 12px; + padding: 8px; + border: 1px solid #eee; + border-radius: 8px; + transition: background-color 0.3s ease; + //width: 100%; /* Keep cards from growing too wide */ +} + +.target-card:hover { + background-color: #f5f5f5; +} + +.target-card.completed { + background-color: #f0f0f0; +} + +.target-image { + width: 64px; + height: 64px; + object-fit: contain; +} + +.target-details { + display: flex; + flex-direction: column; + gap: 8px; +} + +.target-name { + font-weight: 500; +} + +.target-name span { + color: #666; + font-size: 0.9em; +} + +.pokeball-icon { + width: 20px; + height: 20px; + object-fit: contain; + cursor: pointer; + transition: filter 0.3s ease; + } + +.pokeball-icon.grayscale { + filter: grayscale(100%); +} + +`] +}) +export class PlanPokemonDetailsComponent { + @Input() pokemon_family!: PokemonFamilyEntry; + @Output() pokemonCaughtStatusUpdated = new EventEmitter(); + + evolve_to: Pokemon[] = []; + breed_for: Pokemon[] = []; + + constructor( + public pokemonService: PokemonService, + private planService: PlanService + ) {} + + ngOnInit() { + this.evolve_to = [] + this.breed_for = [] + + this.loadPokemonFamilyInfo(this.pokemon_family); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['pokemon_family']) { + const currentFamily = changes['pokemon_family'].currentValue; + const previousFamily = changes['pokemon_family'].previousValue; + + // Check if there's a meaningful change + if (currentFamily && currentFamily !== previousFamily) { + // Your logic here, e.g., re-fetch data or reset states + this.loadPokemonFamilyInfo(currentFamily); + } + } + } + + loadPokemonFamilyInfo(newFamily: PokemonFamilyEntry) { + const evolveToArray: Pokemon[] = []; + this.evolve_to = [] + newFamily.evolve_to.forEach((target) => { + this.pokemonService.getPokemonFromPFIC(target).subscribe({ + next: (pokemon) => { + if (pokemon) { + evolveToArray.push(pokemon); + } + }, + complete: () => { + this.customSort(evolveToArray); + this.evolve_to = [...evolveToArray]; // Assign once all have completed + }, + error: (error) => { + console.error('Error loading Pokémon:', error); + } + }); + }); + + const breedForArray: Pokemon[] = []; + this.breed_for = [] + newFamily.breed_for.forEach((target) => { + this.pokemonService.getPokemonFromPFIC(target).subscribe({ + next: (pokemon) => { + if (pokemon) { + breedForArray.push(pokemon); + } + }, + complete: () => { + this.customSort(breedForArray); + this.breed_for = [...breedForArray]; // Assign once all have completed + }, + error: (error) => { + console.error('Error loading Pokémon:', error); + } + }); + }); + } + + parsePfic(pfic: string): (number | string)[] { + const parts = pfic.split('-'); + return parts.map(part => /^\d+$/.test(part) ? parseInt(part) : part); + } + + customSort(arr: Pokemon[]): Pokemon[] { + return arr.sort((a, b) => { + const parsedA = this.parsePfic(a.PFIC); + const parsedB = this.parsePfic(b.PFIC); + + for (let i = 0; i < Math.min(parsedA.length, parsedB.length); i++) { + if (parsedA[i] !== parsedB[i]) { + if (typeof parsedA[i] === 'number' && typeof parsedB[i] === 'number') { + return (parsedA[i] as number) - (parsedB[i] as number); + } + return (parsedA[i] as string).localeCompare(parsedB[i] as string); + } + } + + return parsedA.length - parsedB.length; + }); + } + + get hasTargets(): boolean { + return this.pokemon_family.evolve_to.length > 0 || this.pokemon_family.breed_for.length > 0; + } + + isTargetCompleted(pfic: string): boolean { + return this.pokemonService.isTargetCompleted(pfic); + } + + onPokeballClick(event: MouseEvent, target: Pokemon) { + event.stopPropagation(); + if (target) { + this.pokemonService.toggleCatch(target.PFIC).subscribe( + plan => { + this.planService.updateCaughtCount(this.pokemon_family); + } + ); + } + } +} \ No newline at end of file diff --git a/src/app/features/plan/plan-pokemon/plan-pokemon.component.ts b/src/app/features/plan/plan-pokemon/plan-pokemon.component.ts index 7a7c1e9..0db2a16 100644 --- a/src/app/features/plan/plan-pokemon/plan-pokemon.component.ts +++ b/src/app/features/plan/plan-pokemon/plan-pokemon.component.ts @@ -1,12 +1,13 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter, ChangeDetectorRef, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { PlanPokemon } from '../../../core/models/plan.model'; +import { PokemonFamilyEntry } from '../../../core/models/plan.model'; import { LazyImgDirective } from '../../../shared/directives/lazy-img.directive'; import { PokemonService } from '../../../core/services/pokemon.service'; +import { Pokemon } from '../../../core/models/pokemon.model'; // Define an interface for the status update event interface PokemonStatusEvent { @@ -35,58 +36,65 @@ interface PokemonStatusEvent {
- {{ pokemon.name }} - - ({{ pokemon.form_name }}) + {{ this.representative_pokemon?.Name }} + + + : {{ this.pokemon_family.Male }} + + + : {{ this.pokemon_family.Female }} + + + : {{ this.pokemon_family.Any }} +
- {{ pokemon.catch_count }} + {{ pokemon_family.catch_count }}
- +

Evolution Targets

- {{ target.name }} - ({{ target.form_name }}) + {{ target.Name }} + ({{ target.Form }})
- {{ target.method }} - Need: {{ target.count }} + Method _PLACEDHOLDER_
@@ -94,30 +102,29 @@ interface PokemonStatusEvent {
- +

Breeding Targets

- {{ target.name }} - ({{ target.form_name }}) + {{ target.Name }} + ({{ target.Form }})
Breed - Need: {{ target.count }}
@@ -252,13 +259,97 @@ interface PokemonStatusEvent { `] }) export class PlanPokemonComponent { - @Input() pokemon!: PlanPokemon; + @Input() pokemon_family!: PokemonFamilyEntry; @Output() statusUpdate = new EventEmitter(); - constructor(public pokemonService: PokemonService) {} + representative_pokemon: Pokemon | null = null; + evolve_to: Pokemon[] = []; + breed_for: Pokemon[] = []; + + constructor( + public pokemonService: PokemonService, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit() { + this.representative_pokemon = null; + this.evolve_to = [] + this.breed_for = [] + + this.handlePokemonFamilyChange(this.pokemon_family); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['pokemon_family']) { + const currentFamily = changes['pokemon_family'].currentValue; + const previousFamily = changes['pokemon_family'].previousValue; + + // Check if there's a meaningful change + if (currentFamily && currentFamily !== previousFamily) { + // Your logic here, e.g., re-fetch data or reset states + this.handlePokemonFamilyChange(currentFamily); + } + } + } + + private handlePokemonFamilyChange(newFamily: PokemonFamilyEntry) { + // This function contains logic to handle the input change. + // For example, resetting component states or fetching additional data. + console.log('Pokemon family has changed:', newFamily); + + this.representative_pokemon = null; + this.evolve_to = [] + this.breed_for = [] + + this.pokemonService.getPokemonFromPFIC(this.pokemon_family.representative).subscribe({ + next: (pokemon) => { + this.representative_pokemon = pokemon + }, + error: (error) => { + console.error('Error loading Pokemon:', error); + this.cdr.markForCheck(); + } + }); + + const evolveToArray: Pokemon[] = []; + newFamily.evolve_to.forEach((target) => { + this.pokemonService.getPokemonFromPFIC(target).subscribe({ + next: (pokemon) => { + if (pokemon) { + evolveToArray.push(pokemon); + } + }, + complete: () => { + this.customSort(evolveToArray); + this.evolve_to = [...evolveToArray]; // Assign once all have completed + }, + error: (error) => { + console.error('Error loading Pokémon:', error); + } + }); + }); + + const breedForArray: Pokemon[] = []; + newFamily.breed_for.forEach((target) => { + this.pokemonService.getPokemonFromPFIC(target).subscribe({ + next: (pokemon) => { + if (pokemon) { + breedForArray.push(pokemon); + } + }, + complete: () => { + this.customSort(breedForArray); + this.breed_for = [...breedForArray]; // Assign once all have completed + }, + error: (error) => { + console.error('Error loading Pokémon:', error); + } + }); + }); + } get hasTargets(): boolean { - return this.pokemon.evolve_to.length > 0 || this.pokemon.breed_for.length > 0; + return this.pokemon_family.evolve_to.length > 0 || this.pokemon_family.breed_for.length > 0; } isTargetCompleted(pfic: string): boolean { @@ -271,14 +362,14 @@ export class PlanPokemonComponent { let evolveCount = 0; // Calculate breeding needs - if (this.pokemon.breed_for.length > 0) { + if (this.pokemon_family.breed_for.length > 0) { breedCount = 1; // We only need one for breeding, regardless of how many we breed } // Calculate evolution needs - this.pokemon.evolve_to.forEach(target => { - if (!this.isTargetCompleted(target.pfic)) { - evolveCount += target.count; + this.pokemon_family.evolve_to.forEach(target => { + if (!this.isTargetCompleted(target)) { + evolveCount += 1; } }); @@ -287,23 +378,54 @@ export class PlanPokemonComponent { updateCatchCount() { const newCount = this.calculateTotalNeeded(); - if (newCount !== this.pokemon.catch_count) { - this.pokemon.catch_count = newCount; + if (newCount !== this.pokemon_family.catch_count) { + this.pokemon_family.catch_count = newCount; if (newCount === 0) { // Emit event to move to completed section this.statusUpdate.emit({ - pfic: this.pokemon.pfic, + pfic: this.pokemon_family.representative, caught: true, completed: true }); - } else if (newCount > 0 && this.pokemon.catch_count === 0) { + } else if (newCount > 0 && this.pokemon_family.catch_count === 0) { // Emit event to move back to active section this.statusUpdate.emit({ - pfic: this.pokemon.pfic, + pfic: this.pokemon_family.representative, caught: false, completed: false }); } } } + + getRepresentativePokemon() { + return this.pokemonService.getPokemonFromPFIC(this.pokemon_family.representative) + } + + trackByPfic(index: number, item: any): string { + return item.PFIC; // Assuming PFIC or another unique identifier is available + } + + parsePfic(pfic: string): (number | string)[] { + const parts = pfic.split('-'); + return parts.map(part => /^\d+$/.test(part) ? parseInt(part) : part); + } + + customSort(arr: Pokemon[]): Pokemon[] { + return arr.sort((a, b) => { + const parsedA = this.parsePfic(a.PFIC); + const parsedB = this.parsePfic(b.PFIC); + + for (let i = 0; i < Math.min(parsedA.length, parsedB.length); i++) { + if (parsedA[i] !== parsedB[i]) { + if (typeof parsedA[i] === 'number' && typeof parsedB[i] === 'number') { + return (parsedA[i] as number) - (parsedB[i] as number); + } + return (parsedA[i] as string).localeCompare(parsedB[i] as string); + } + } + + return parsedA.length - parsedB.length; + }); + } } \ No newline at end of file diff --git a/src/app/features/plan/plan-pokemon/plan-pokemonV2.component.ts b/src/app/features/plan/plan-pokemon/plan-pokemonV2.component.ts new file mode 100644 index 0000000..c7fd82e --- /dev/null +++ b/src/app/features/plan/plan-pokemon/plan-pokemonV2.component.ts @@ -0,0 +1,265 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectorRef, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatIconModule } from '@angular/material/icon'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { PokemonFamilyEntry } from '../../../core/models/plan.model'; +import { LazyImgDirective } from '../../../shared/directives/lazy-img.directive'; +import { PokemonService } from '../../../core/services/pokemon.service'; +import { Pokemon } from '../../../core/models/pokemon.model'; +import { MatCardModule } from '@angular/material/card'; + +// Define an interface for the status update event +interface PokemonStatusEvent { + pfic: string; + caught: boolean; + completed?: boolean; // Make completed optional + } + +@Component({ + selector: 'app-plan-pokemonV2', + standalone: true, + imports: [ + CommonModule, + MatExpansionModule, + MatIconModule, + MatChipsModule, + MatTooltipModule, + LazyImgDirective, + MatCardModule + ], + template: ` + +
+ + +
+
+ {{ this.representative_pokemon?.Name }} + + + : {{ this.pokemon_family.Male }} + + + : {{ this.pokemon_family.Female }} + + + : {{ this.pokemon_family.Any }} + + +
+ +
+ + {{ this.pokemon_family.catch_count - this.pokemon_family.caught_count }} +
+
+
+
+
+
+
+ `, + styles: [` + .pokemon-row{ + margin:5px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + } + + .pokemon-row:hover { + transform: translateY(-4px); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + } + + .pokemon-row.selected { + border: 2px solid #4CAF50; + transform: translateY(-2px); + } + + .pokemon-header { + display: flex; + align-items: center; + gap: 16px; + width: 100%; + } + + .pokemon-thumbnail { + width: 48px; + height: 48px; + object-fit: contain; + } + + .pokemon-info { + display: flex; + justify-content: space-between; + align-items: center; + flex-grow: 1; + } + + .pokemon-name { + font-weight: 500; + font-size: 1.1em; + } + + .form-name { + color: #666; + font-size: 0.9em; + margin-left: 4px; + } + + .catch-info { + display: flex; + align-items: center; + gap: 8px; + min-width: 80px; + } + + .pokeball-icon { + width: 24px; + height: 24px; + cursor: pointer; + transition: filter 0.3s ease; + } + + .grayscale { + filter: grayscale(100%); + } + + .catch-count { + font-weight: 500; + color: #4CAF50; + } + + .progress-bar-container { + width: 100%; + height: 4px; + background: #e0e0e0; + border-radius: 2px; + margin-top: 8px; + overflow: hidden; + } + + .progress-bar { + height: 100%; + background: #4CAF50; + transition: width 0.3s ease; + } + `] +}) +export class PlanPokemonV2Component { + @Input() pokemon_family!: PokemonFamilyEntry; + @Input() isSelected = false; + @Output() statusUpdate = new EventEmitter(); + @Output() familySelected = new EventEmitter(); + + representative_pokemon: Pokemon | null = null; + catch_count = 0; + + constructor( + public pokemonService: PokemonService, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit() { + this.representative_pokemon = null; + this.handlePokemonFamilyChange(this.pokemon_family); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['pokemon_family']) { + const currentFamily = changes['pokemon_family'].currentValue; + const previousFamily = changes['pokemon_family'].previousValue; + + // Check if there's a meaningful change + if (currentFamily && currentFamily !== previousFamily) { + // Your logic here, e.g., re-fetch data or reset states + this.handlePokemonFamilyChange(currentFamily); + } + } + } + + private handlePokemonFamilyChange(newFamily: PokemonFamilyEntry) { + // This function contains logic to handle the input change. + // For example, resetting component states or fetching additional data. + console.log('Pokemon family has changed:', newFamily); + + this.representative_pokemon = null; + + this.pokemonService.getPokemonFromPFIC(this.pokemon_family.representative).subscribe({ + next: (pokemon) => { + this.representative_pokemon = pokemon + }, + error: (error) => { + console.error('Error loading Pokemon:', error); + this.cdr.markForCheck(); + } + }); + + this.updateCatchCount(); + } + + get hasTargets(): boolean { + return this.pokemon_family.evolve_to.length > 0 || this.pokemon_family.breed_for.length > 0; + } + + isTargetCompleted(pfic: string): boolean { + return this.pokemonService.isTargetCompleted(pfic); + } + + calculateTotalCaught(): number { + let count = 0; + + this.pokemon_family.evolve_to.forEach(target => { + if (this.isTargetCompleted(target)) { + count += 1; + } + }); + + this.pokemon_family.breed_for.forEach(target => { + if (this.isTargetCompleted(target)) { + count += 1; + } + }); + + return count; + } + + calculateCatchProgress(): number { + const totalNeeded = this.pokemon_family.catch_count; + const caughtCount = this.calculateTotalCaught(); + return (caughtCount / totalNeeded) * 100; + } + + updateCatchCount() { + const newCount = this.calculateTotalCaught(); + this.catch_count = this.pokemon_family.catch_count - newCount; + console.log(this.catch_count) + } + + getRepresentativePokemon() { + return this.pokemonService.getPokemonFromPFIC(this.pokemon_family.representative) + } + + trackByPfic(index: number, item: any): string { + return item.PFIC; // Assuming PFIC or another unique identifier is available + } + + onSelect() { + this.familySelected.emit(this.pokemon_family); + } +} + diff --git a/src/app/features/plan/plan.component.ts b/src/app/features/plan/plan.component.ts index dcef26b..4c39dec 100644 --- a/src/app/features/plan/plan.component.ts +++ b/src/app/features/plan/plan.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { FormsModule } from '@angular/forms'; @@ -6,6 +6,7 @@ import { PlanGameComponent } from './plan-game/plan-game.component'; import { PlanService } from '../../core/services/plan.service'; import { GamePlan } from '../../core/models/plan.model'; import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component"; +import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; @Component({ selector: 'app-plan', @@ -15,7 +16,8 @@ import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component"; MatCardModule, FormsModule, PlanGameComponent, - PlanPokemonComponent + PlanPokemonComponent, + ScrollingModule ], template: `
@@ -34,13 +36,14 @@ import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component";

{{ selectedGame.game_name }} - Pokémon to Catch

-
- -
+ +
+ +
+
`, @@ -90,13 +93,23 @@ import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component"; border-radius: 8px; margin-bottom: 20px; } + .pokemon-viewport { + flex: 1; + overflow-y: auto; + padding: 16px; + background: #f5f5f5; + border-radius: 8px; + margin-bottom: 20px; + } `] }) export class PlanComponent implements OnInit { + @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport; + gamePlans: GamePlan[] = []; selectedGame: GamePlan | null = null; - constructor(private planService: PlanService) {} + constructor(private planService: PlanService, private cdr: ChangeDetectorRef) {} ngOnInit() { this.loadPlan(); @@ -114,10 +127,20 @@ export class PlanComponent implements OnInit { } selectGame(game: GamePlan) { + if (this.viewport) { + 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(); this.selectedGame = game; + this.cdr.detectChanges(); } onPokemonStatusUpdate(event: { pfic: string, caught: boolean }) { this.loadPlan(); } + + trackByPfic(index: number, item: any): string { + return item.key; // Assuming PFIC or another unique identifier is available + } } \ No newline at end of file diff --git a/src/app/features/plan/planV2.component.ts b/src/app/features/plan/planV2.component.ts new file mode 100644 index 0000000..3a77d2b --- /dev/null +++ b/src/app/features/plan/planV2.component.ts @@ -0,0 +1,274 @@ +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'; +import { PlanGameComponent } from './plan-game/plan-game.component'; +import { PlanService } from '../../core/services/plan.service'; +import { GamePlan, PokemonFamilyEntry } from '../../core/models/plan.model'; +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', + standalone: true, + imports: [ + CommonModule, + MatCardModule, + FormsModule, + PlanGameComponent, + PlanPokemonV2Component, + ScrollingModule, + PlanPokemonDetailsComponent +], + template: ` +
+
+
+
+ +
+
+
+ +
+
+

{{ selectedGame.game_name }} - Game Stats

+ +
+ +
+
+ +
+ +
+
+
+ +
+

{{ selectedPokemon?.representative }} - Details

+ +
+
+
+
+ `, + styles: [` + .plan-container { + display: flex; + height: calc(100vh - 64px); /* Adjust based on your header height */ + overflow: hidden; + } + + .games-section { + width: 220px; + padding: 20px; + overflow-y: auto; + background: #f5f5f5; + border-right: 1px solid #ddd; + } + + .games-scroll { + width: 100%; + } + + .games-list { + display: flex; + flex-direction: column; + gap: 16px; + } + + .content-section { + flex: 1; + display: flex; + flex-direction: column; + padding: 20px; + overflow: hidden; + } + + .middle-section { + flex: 2; + display: flex; + flex-direction: row; + margin-right: 20px; + } + + .game-stats { + padding-bottom: 16px; + border-bottom: 1px solid #ddd; + margin-bottom: 16px; + } + + .pokemon-section { + flex: 1; + min-height: 0; /* Important for Firefox */ + display: flex; + flex-direction: row; + margin-right: 15px; + } + + .pokemon-viewport { + flex: 1; + overflow-y: auto; + padding: 16px; + background: #f5f5f5; + border-radius: 8px; + margin-bottom: 20px; + } + + .details-section { + flex: 1; + padding: 16px; + background: #fff; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width:15% + } + `] +}) +export class PlanV2Component implements OnInit { + @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport; + + gamePlans: GamePlan[] = []; + 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 searchService: SearchService + ) {} + + ngOnInit() { + this.authService.isAuthenticated$.subscribe((isAuthenticated) => { + if (isAuthenticated) { + this.loadPlan(); + 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() { + this.planService.getPlan().subscribe( + plan => { + this.gamePlans = plan; + if (!this.selectedGame && plan.length > 0) { + this.selectedGame = plan[0]; + } + } + ); + } + + selectGame(game: GamePlan) { + if (this.viewport) { + //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(); + this.selectedGame = game; + this.cdr.detectChanges(); + } + + onPokemonStatusUpdate(event: { pfic: string, caught: boolean }) { + this.loadPlan(); + } + + selectPokemon(pokemon: any) { + this.selectedPokemon = pokemon; + } + + 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 d8bd8fc..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 }}) - - - - -
-