From d866c25fb9e26973c1bdfa4671a0baff1fde0d00 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Nov 2024 14:53:00 +0000 Subject: [PATCH] - Move to a 3 colum split for the plan view --- src/app/app.routes.ts | 4 +- src/app/core/models/plan.model.ts | 1 + .../plan/plan-game/plan-game.component.ts | 18 +- .../plan-pokemon-details.component.ts | 227 ++++++++++++ .../plan-pokemon/plan-pokemon.component.ts | 101 +++--- .../plan-pokemon/plan-pokemonV2.component.ts | 325 ++++++++++++++++++ src/app/features/plan/planV2.component.ts | 189 ++++++++++ 7 files changed, 802 insertions(+), 63 deletions(-) create mode 100644 src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts create mode 100644 src/app/features/plan/plan-pokemon/plan-pokemonV2.component.ts create mode 100644 src/app/features/plan/planV2.component.ts 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 3bb379d..c9e1bce 100644 --- a/src/app/core/models/plan.model.ts +++ b/src/app/core/models/plan.model.ts @@ -4,6 +4,7 @@ export interface GamePlan { } export interface PokemonFamilyEntry { + family_pfic?: string; representative: string; catch_count: number; evolve_to: 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 08a797c..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: [` @@ -78,12 +72,18 @@ export class PlanGameComponent { sum += this.game.pokemon[family].catch_count; } return sum - //return this.game.pokemon.values().reduce((sum, pokemon) => sum + pokemon.catch_count, 0); } getGameBoxArt(): string { - // You'll need to implement this to return the correct box art URL - return `/assets/images/games/_${this.game.game_name.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..3cf5e4a --- /dev/null +++ b/src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts @@ -0,0 +1,227 @@ +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'; + +@Component({ +selector: 'app-plan-pokemon-details', +standalone: true, +imports: [ + CommonModule, + MatChipsModule +], +template: ` +
+ +
+

Evolution Targets

+
+
+ +
+
+ {{ target.Name }} + ({{ target.Form }}) +
+ {{target?.EvolutionMethod}} +
+
+
+
+
+ + +
+

Breeding Targets

+
+
+ +
+
+ {{ target.Name }} + ({{ target.Form }}) +
+ + Breed + +
+
+
+
+
+
+`, +styles: [` + .targets-grid { + padding: 16px 8px; + display: flex; + flex-direction: column; + gap: 24px; + } + + .target-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 16px; + } + + .target-card { + display: flex; + align-items: center; + gap: 12px; + padding: 8px; + border: 1px solid #eee; + border-radius: 8px; + transition: background-color 0.3s ease; + + &:hover { + background-color: #f5f5f5; + } + + &.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; + + span { + color: #666; + font-size: 0.9em; + } + } +`] +}) +export class PlanPokemonDetailsComponent { + @Input() pokemon_family!: PokemonFamilyEntry; + evolve_to: Pokemon[] = []; + breed_for: Pokemon[] = []; + + constructor( + public pokemonService: PokemonService + ) {} + + 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[] = []; + 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); + } + }); + }); + } + + 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); + } +} \ 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 ed8fb9f..0db2a16 100644 --- a/src/app/features/plan/plan-pokemon/plan-pokemon.component.ts +++ b/src/app/features/plan/plan-pokemon/plan-pokemon.component.ts @@ -77,7 +77,7 @@ interface PokemonStatusEvent {

Evolution Targets

@@ -275,45 +275,8 @@ export class PlanPokemonComponent { this.representative_pokemon = null; this.evolve_to = [] this.breed_for = [] - this.cdr.detectChanges(); - 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(); - } - }); - - for(const target of this.pokemon_family.evolve_to) { - this.pokemonService.getPokemonFromPFIC(target).subscribe({ - next: (pokemon) => { - if(pokemon) { - this.evolve_to.push(pokemon) - } - }, - error: (error) => { - console.error('Error loading Pokemon:', error); - this.cdr.markForCheck(); - } - }); - } - - for(const target of this.pokemon_family.breed_for) { - this.pokemonService.getPokemonFromPFIC(target).subscribe({ - next: (pokemon) => { - if(pokemon) { - this.breed_for.push(pokemon) - } - }, - error: (error) => { - console.error('Error loading Pokemon:', error); - this.cdr.markForCheck(); - } - }); - } + this.handlePokemonFamilyChange(this.pokemon_family); } ngOnChanges(changes: SimpleChanges) { @@ -337,7 +300,6 @@ export class PlanPokemonComponent { this.representative_pokemon = null; this.evolve_to = [] this.breed_for = [] - this.cdr.detectChanges(); this.pokemonService.getPokemonFromPFIC(this.pokemon_family.representative).subscribe({ next: (pokemon) => { @@ -349,33 +311,41 @@ export class PlanPokemonComponent { } }); - for(const target of this.pokemon_family.evolve_to) { + const evolveToArray: Pokemon[] = []; + newFamily.evolve_to.forEach((target) => { this.pokemonService.getPokemonFromPFIC(target).subscribe({ next: (pokemon) => { - if(pokemon) { - this.evolve_to.push(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 Pokemon:', error); - this.cdr.markForCheck(); + console.error('Error loading Pokémon:', error); } }); - } + }); - for(const target of this.pokemon_family.breed_for) { + const breedForArray: Pokemon[] = []; + newFamily.breed_for.forEach((target) => { this.pokemonService.getPokemonFromPFIC(target).subscribe({ next: (pokemon) => { - if(pokemon) { - this.breed_for.push(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 Pokemon:', error); - this.cdr.markForCheck(); + console.error('Error loading Pokémon:', error); } }); - } + }); } get hasTargets(): boolean { @@ -431,4 +401,31 @@ export class PlanPokemonComponent { 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..6101260 --- /dev/null +++ b/src/app/features/plan/plan-pokemon/plan-pokemonV2.component.ts @@ -0,0 +1,325 @@ +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 }} + + +
+ +
+ + {{ pokemon_family.catch_count }} +
+
+
+
+ `, + styles: [` + .pokemon-panel { + margin-bottom: 8px; + } + + .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; + } + + .targets-grid { + padding: 16px 8px; + gap: 24px; + } + + .target-section { + h4 { + margin: 0 0 12px 0; + color: #666; + } + } + + .target-cards { + gap: 16px; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + .target-card { + display: flex; + align-items: center; + gap: 12px; + padding: 8px; + border: 1px solid #eee; + border-radius: 8px; + transition: background-color 0.3s ease; + + &:hover { + background-color: #f5f5f5; + } + + &.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; + + span { + color: #666; + font-size: 0.9em; + } + } + + mat-chip-list { + display: flex; + gap: 4px; + } + `] +}) +export class PlanPokemonV2Component { + @Input() pokemon_family!: PokemonFamilyEntry; + @Input() isSelected = false; + @Output() statusUpdate = new EventEmitter(); + @Output() familySelected = new EventEmitter(); + + representative_pokemon: Pokemon | null = null; + + 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(); + } + }); + } + + 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); + } + + calculateTotalNeeded(): number { + let total = 1; // Initial catch + let breedCount = 0; + let evolveCount = 0; + + // Calculate breeding needs + 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_family.evolve_to.forEach(target => { + if (!this.isTargetCompleted(target)) { + evolveCount += 1; + } + }); + + return total + breedCount + evolveCount; + } + + updateCatchCount() { + const newCount = this.calculateTotalNeeded(); + 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_family.representative, + caught: true, + completed: true + }); + } else if (newCount > 0 && this.pokemon_family.catch_count === 0) { + // Emit event to move back to active section + this.statusUpdate.emit({ + 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 + } + + onSelect() { + this.familySelected.emit(this.pokemon_family); + } +} \ 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..94df193 --- /dev/null +++ b/src/app/features/plan/planV2.component.ts @@ -0,0 +1,189 @@ +import { ChangeDetectorRef, Component, OnInit, ViewChild } 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"; + +@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: 11%; + 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; + + constructor(private planService: PlanService, private cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.loadPlan(); + } + + 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 + } +} +